diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..210d3ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*.java] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2fb638f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto + +*.sh text eol=lf +gradlew text eol=lf +*.bat text eol=crlf + +*.jar binary diff --git a/.github/ISSUE_TEMPLATE/behavior-bug.yml b/.github/ISSUE_TEMPLATE/behavior-bug.yml new file mode 100644 index 0000000..80465e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/behavior-bug.yml @@ -0,0 +1,76 @@ +name: Behavior Bug +description: Report behavior related issues, where Folia does not work like vanilla. Paper and Bukkit plugins are not guaranteed to work on Folia. Do not submit bug reports for plugin issues here. +labels: [ "status: needs triage", "type: bug" ] +body: + - 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: Folia 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 + [10:00:11 INFO]: Checking version, please wait... + [10:00:12 INFO]: This server is running Folia version git-Folia-"5b74945" (MC: 1.19.4) (Implementing API version 1.19.4-R0.1-SNAPSHOT) (Git: 5b74945) + You are running the latest version + Previous version: git-Paper-481 (MC: 1.19.4) + ``` + +
+ 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 + + - type: markdown + attributes: + value: | + Before submitting this issue, please ensure the following: + + 1. You are running the latest version of Folia from [our downloads page](https://papermc.io/downloads). + 2. You searched for and ensured there isn't already an open issue regarding this. + 3. Your version of Minecraft is supported by Folia. + + If you think you have a bug but are not sure, feel free to ask the `#folia-help` channel of our + [Discord](https://discord.gg/papermc). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b723d47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false +contact_links: + - name: PaperMC Discord + url: https://discord.gg/papermc + about: If you are having minor issues, come ask us on our Discord server! + - name: Exploit Report + url: https://discord.gg/papermc + about: | + Due to GitHub not currently allowing private issues, exploit reports are currently handled via our Discord. + To report an exploit, see the #paper-exploit-report channel. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..b1284fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,44 @@ +name: Feature Request +description: Suggest an idea for Folia +labels: [ "status: needs triage", "type: feature" ] +body: + - type: markdown + attributes: + value: | + Thank you for filling out a feature request for Folia! 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 Folia. + + - 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. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/performance-problems.yml b/.github/ISSUE_TEMPLATE/performance-problems.yml new file mode 100644 index 0000000..42ba14f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance-problems.yml @@ -0,0 +1,84 @@ +name: Performance Problem +description: Report performance related problems or other areas of concern +labels: [ "status: needs triage", "type: performance" ] +body: + - type: markdown + attributes: + value: | + Before creating an issue regarding server performance, please consider reaching out for support in the + `#folia-help` channel of [our Discord](https://discord.gg/papermc)! + + - type: input + attributes: + label: Profile link + description: We ask that all profiles are a link, not a screenshot. Screenshots inhibit our ability to figure out the real cause of the issue. + placeholder: "Example: https://spark.lucko.me/abcdefg12h" + 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://paste.gg. + value: | + ``` + Paste configs or paste.gg 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: Folia 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 + [10:00:11 INFO]: Checking version, please wait... + [10:00:12 INFO]: This server is running Folia version git-Folia-"5b74945" (MC: 1.19.4) (Implementing API version 1.19.4-R0.1-SNAPSHOT) (Git: 5b74945) + You are running the latest version + Previous version: git-Paper-481 (MC: 1.19.4) + ``` + +
+ 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 + - type: markdown + attributes: + value: | + Before submitting this issue, please ensure the following: + + 1. You are running the latest version of Folia from [our downloads page](https://papermc.io/downloads). + 2. You searched for and ensured there isn't already an open issue regarding this. + 3. Your version of Minecraft is supported by Folia. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/server-crash-or-stacktrace.yml b/.github/ISSUE_TEMPLATE/server-crash-or-stacktrace.yml new file mode 100644 index 0000000..7816e0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/server-crash-or-stacktrace.yml @@ -0,0 +1,76 @@ +name: Server crash or Stacktrace +description: Report server crashes or scary stacktraces +labels: [ "status: needs triage" ] +body: + - 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://paste.gg. + value: | + ``` + paste your stack trace or a paste.gg 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: Folia 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 + [10:00:11 INFO]: Checking version, please wait... + [10:00:12 INFO]: This server is running Folia version git-Folia-"5b74945" (MC: 1.19.4) (Implementing API version 1.19.4-R0.1-SNAPSHOT) (Git: 5b74945) + You are running the latest version + Previous version: git-Paper-481 (MC: 1.19.4) + ``` + +
+ 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 + + - type: markdown + attributes: + value: | + Before submitting this issue, please ensure the following: + + 1. You are running the latest version of Folia from [our downloads page](https://papermc.io/downloads). + 2. Your version of Minecraft is supported by Folia. + + If your server crash log contains `DO NOT REPORT THIS TO FOLIA`, 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. \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d1fabe2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Patch and Build + +on: + push: + branches: [ "**" ] + pull_request: + +jobs: + build: + # 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: Checkout Git Repository + uses: actions/checkout@v3 + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Configure Git User Details + run: git config --global user.email "actions@github.com" && git config --global user.name "Github Actions" + - name: Apply Patches + run: ./gradlew applyPatches + - name: Build + run: ./gradlew createMojmapPaperclipJar + - name: Upload Paperclip Jar + uses: actions/upload-artifact@v4 + with: + name: Mizi-paperclip + path: build/libs/mizi-paperclip-*-mojmap.jar diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab5b3e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +.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 +build/ +bin/ +dist/ +manifest.mf + +# Mac filesystem dust +.DS_Store/ +.DS_Store + +# intellij +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Linux temp files +*~ + +# other stuff +run/ + +Folia-Server +Folia-API +paper-api-generator + +!gradle/wrapper/gradle-wrapper.jar +/Mizi-Server +/Mizi-API diff --git a/PATCHES-LICENSE b/PATCHES-LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/PATCHES-LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/PROJECT_DESCRIPTION.md b/PROJECT_DESCRIPTION.md new file mode 100644 index 0000000..7ffd17a --- /dev/null +++ b/PROJECT_DESCRIPTION.md @@ -0,0 +1,3 @@ +# Project overview + +This page has been moved to [the PaperMC documentation](https://docs.papermc.io/folia/reference/overview) site. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfbfca7 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +## Overview + +Mizi is a fork of [Folia](https://github.com/PaperMC/Folia) containing performance patches from [Pufferfish](https://github.com/pufferfish-gg/Pufferfish) and [Lithium](https://github.com/CaffeineMC/lithium-fabric) and some patches from [Purpur](https://github.com/PurpurMC/Purpur) that I use on my server. + +This fork is made for my own personal server and I **do not** guarantee any stability and I don't encourage you to use it for the performance gains but you're free to do whatever you want. + +**And it's important to keep in mind that mixing different patches together is *more likely than not* to cause unexpected issues** + +Especially since this fork removes folia's safeguard against loading *potentially* unsafe plugins (plugins that aren't thread-safe and use the main thread) so if you intend to use plugins on this fork **make sure** that it will work on folia. + +```markdown +There is no more main thread. I expect _every_ single plugin +that exists to require _some_ level of modification to function +in Folia. Additionally, multithreading of _any kind_ introduces +possible race conditions in plugin held data - so, there are bound +to be changes that need to be made. + +So, have your expectations for compatibility at 0. +``` + +## FAQ + +#### How to download builds? +Builds are available to download through Github Actions. + +#### Any questions about Folia's designs, limitations or other things? +[visit folia's readme](https://github.com/PaperMC/Folia/blob/master/README.md) + + +## License +The PATCHES-LICENSE file describes the license for api & server patches made by SpottedLeaf (from folia), +all other patches are subject to their original licenses stated in the original patches and/or the projects' licenses that they were taken from. + +The fork is based off of PaperMC's fork example found [here](https://github.com/PaperMC/paperweight-examples). +As such, it contains modifications to it in this project, please see the repository for license information +of modified files. diff --git a/REGION_LOGIC.md b/REGION_LOGIC.md new file mode 100644 index 0000000..b309809 --- /dev/null +++ b/REGION_LOGIC.md @@ -0,0 +1,3 @@ +# Region Logic + +This page has been moved to [the PaperMC documentation](https://docs.papermc.io/folia/reference/region-logic) site. diff --git a/build-data/dev-imports.txt b/build-data/dev-imports.txt new file mode 100644 index 0000000..f35428a --- /dev/null +++ b/build-data/dev-imports.txt @@ -0,0 +1,10 @@ +# 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 \ No newline at end of file diff --git a/build-data/reobf-mappings-patch.tiny b/build-data/reobf-mappings-patch.tiny new file mode 100644 index 0000000..e975a3c --- /dev/null +++ b/build-data/reobf-mappings-patch.tiny @@ -0,0 +1,18 @@ +# 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. +# +# If you are adding mappings patches which are correcting for issues in paperweight's reobf mappings generation, +# unrelated to any changes in your patches, we ask that you PR the mapping to Paper so more users can benefit rather +# than keep the fix for your own fork. If the mappings patch is there to correct reobf for changes made in your patches, +# then obviously it doesn't make any sense to PR them upstream. + +tiny 2 0 mojang+yarn spigot diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..45f2d29 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,121 @@ +import io.papermc.paperweight.tasks.RebuildGitPatches + +plugins { + java + `maven-publish` + id("io.papermc.paperweight.patcher") version "1.7.7-SNAPSHOT" +} + +val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/" + +repositories { + mavenCentral() + maven(paperMavenPublicUrl) { + content { onlyForConfigurations(configurations.paperclip.name) } + } +} + +dependencies { + remapper("net.fabricmc:tiny-remapper:0.10.3:fat") + decompiler("org.vineflower:vineflower:1.11.0-20241204.173358-53") + paperclip("io.papermc:paperclip:3.0.3") +} + +allprojects { + apply(plugin = "java") + apply(plugin = "maven-publish") + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } +} + +subprojects { + tasks.withType { + options.encoding = Charsets.UTF_8.name() + options.release.set(21) + options.isFork = true + } + tasks.withType { + options.encoding = Charsets.UTF_8.name() + } + tasks.withType { + filteringCharset = Charsets.UTF_8.name() + } + + repositories { + mavenCentral() + maven(paperMavenPublicUrl) + } +} + +paperweight { + serverProject.set(project(":mizi-server")) + + remapRepo.set(paperMavenPublicUrl) + decompileRepo.set(paperMavenPublicUrl) + + usePaperUpstream(providers.gradleProperty("paperRef")) { + withPaperPatcher { + apiPatchDir.set(layout.projectDirectory.dir("patches/api")) + apiOutputDir.set(layout.projectDirectory.dir("Mizi-API")) + + serverPatchDir.set(layout.projectDirectory.dir("patches/server")) + serverOutputDir.set(layout.projectDirectory.dir("Mizi-Server")) + } + patchTasks.register("generatedApi") { + isBareDirectory = true + upstreamDirPath = "paper-api-generator/generated" + patchDir = layout.projectDirectory.dir("patches/generatedApi") + outputDir = layout.projectDirectory.dir("paper-api-generator/generated") + } + + } +} + +//tasks.generateDevelopmentBundle { + // apiCoordinates.set("dev.mizi:mizi-api") + // libraryRepositories.addAll( + // "https://repo.maven.apache.org/maven2/", + // paperMavenPublicUrl, + // ) +// } + +allprojects { + publishing { + repositories { + maven("https://repo.papermc.io/repository/maven-snapshots/") { + name = "paperSnapshots" + credentials(PasswordCredentials::class) + } + } + } +} + +publishing { + if (project.hasProperty("publishDevBundle")) { + publications.create("devBundle") { + artifact(tasks.generateDevelopmentBundle) { + artifactId = "dev-bundle" + } + } + } +} + +tasks.withType { + filterPatches.set(false) +} + +tasks.register("printMinecraftVersion") { + doLast { + println(providers.gradleProperty("mcVersion").get().trim()) + } +} + +tasks.register("printPaperVersion") { + doLast { + println(project.version) + } +} diff --git a/folia.png b/folia.png new file mode 100644 index 0000000..1af6243 Binary files /dev/null and b/folia.png differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..322499c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +group=dev.mizi +version=1.21.4-R0.1-SNAPSHOT +mcVersion=1.21.4 + +paperRef=b746d9df0b6b7765478b2a72c2d963b6e668fa35 + +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.vfs.watch=false + +org.gradle.jvmargs=-Xmx32G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4eaec46 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..080cb72 --- /dev/null +++ b/install.bat @@ -0,0 +1 @@ +./gradlew publishToMavenLocal \ No newline at end of file diff --git a/jar.bat b/jar.bat new file mode 100644 index 0000000..770ce43 --- /dev/null +++ b/jar.bat @@ -0,0 +1 @@ +./gradlew createMojmapPaperclipJar \ No newline at end of file diff --git a/patch.bat b/patch.bat new file mode 100644 index 0000000..f87080c --- /dev/null +++ b/patch.bat @@ -0,0 +1 @@ +./gradlew applypatches \ No newline at end of file diff --git a/patches/api/0001-Force-disable-timings.patch b/patches/api/0001-Force-disable-timings.patch new file mode 100644 index 0000000..ff72603 --- /dev/null +++ b/patches/api/0001-Force-disable-timings.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 23 Feb 2023 20:12:48 -0800 +Subject: [PATCH] Force disable timings + +Need a new profiler system with region threading + +diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java +index 95b7cdf0677ef71e6885fa78aa5c75bb500f5f53..89017af09ce32e7a66014fc3aeb50155921252a5 100644 +--- a/src/main/java/co/aikar/timings/Timings.java ++++ b/src/main/java/co/aikar/timings/Timings.java +@@ -145,6 +145,7 @@ public final class Timings { + * @param enabled Should timings be reported + */ + public static void setTimingsEnabled(boolean enabled) { ++ enabled = false; // Folia - region threading - disable timings + if (enabled && !warnedAboutDeprecationOnEnable) { + Bukkit.getLogger().severe(PlainTextComponentSerializer.plainText().serialize(deprecationMessage())); + warnedAboutDeprecationOnEnable = true; diff --git a/patches/api/0002-Region-scheduler-API.patch b/patches/api/0002-Region-scheduler-API.patch new file mode 100644 index 0000000..9ea5e80 --- /dev/null +++ b/patches/api/0002-Region-scheduler-API.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 4 Mar 2023 12:48:43 -0800 +Subject: [PATCH] Region scheduler API + +Add both a location based scheduler, an entity based scheduler, +and a global region scheduler. + +diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index 001465eedafa51ac027a4db51cba6223edfe1171..468f5646da7bc413a6e91e82379e6554cc8b459d 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -557,9 +557,9 @@ public final class SimplePluginManager implements PluginManager { + } + + try { +- server.getScheduler().cancelTasks(plugin); ++ server.getAsyncScheduler().cancelTasks(plugin); // Folia - new schedulers + } catch (Throwable ex) { +- handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " ++ handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " // Folia - new schedulers + + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper + } + +diff --git a/src/main/java/org/bukkit/scheduler/BukkitScheduler.java b/src/main/java/org/bukkit/scheduler/BukkitScheduler.java +index 5c277ac7a61df8106f7c13a1ad8ccb1509338699..446c115f19c4dde0d5e269f8d120bc51ad3898e0 100644 +--- a/src/main/java/org/bukkit/scheduler/BukkitScheduler.java ++++ b/src/main/java/org/bukkit/scheduler/BukkitScheduler.java +@@ -7,6 +7,15 @@ import java.util.function.Consumer; + import org.bukkit.plugin.Plugin; + import org.jetbrains.annotations.NotNull; + ++// Folia start - add new schedulers ++/** ++ * @deprecated Use one of {@link io.papermc.paper.threadedregions.scheduler.RegionScheduler}, ++ * {@link io.papermc.paper.threadedregions.scheduler.AsyncScheduler}, ++ * {@link io.papermc.paper.threadedregions.scheduler.EntityScheduler}, ++ * or {@link io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler} ++ */ ++// Folia end - add new schedulers ++@Deprecated + public interface BukkitScheduler { + + /** diff --git a/patches/api/0003-Add-RegionizedServerInitEvent.patch b/patches/api/0003-Add-RegionizedServerInitEvent.patch new file mode 100644 index 0000000..f9f4c2b --- /dev/null +++ b/patches/api/0003-Add-RegionizedServerInitEvent.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 7 Mar 2023 12:58:48 -0800 +Subject: [PATCH] Add RegionizedServerInitEvent + +This event allows plugins to perform synchronous operations before +any region will tick. Plugins will not have to worry about the +possibility of a region ticking in parallel while listening +to the event. + +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServerInitEvent.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServerInitEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c7be944c5638cb6650624bd622b2ad9d52c5c31d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServerInitEvent.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.threadedregions; ++ ++import org.bukkit.event.HandlerList; ++import org.bukkit.event.server.ServerEvent; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * This event is called after the server is initialised but before the server begins ticking regions in parallel. ++ * Plugins may use this as a hook to run post initialisation logic without worrying about the possibility that ++ * regions are ticking in parallel. ++ */ ++public class RegionizedServerInitEvent extends ServerEvent { ++ ++ private static final HandlerList handlers = new HandlerList(); ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++} diff --git a/patches/api/0004-Purpur-config-files.patch b/patches/api/0004-Purpur-config-files.patch new file mode 100644 index 0000000..468f177 --- /dev/null +++ b/patches/api/0004-Purpur-config-files.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: William Blake Galbreath +Date: Tue, 18 Feb 2020 20:30:03 -0600 +Subject: [PATCH] Purpur config files + + +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index ad816538b30079c62d5e1eb98c6f4b61e12e8d47..f90da51a8d1003a5cba86decbd42470f7f7e9211 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -2283,6 +2283,18 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + } + // Paper end + ++ // Purpur start ++ @NotNull ++ public org.bukkit.configuration.file.YamlConfiguration getPurpurConfig() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ ++ @NotNull ++ public java.util.Properties getServerProperties() { ++ throw new UnsupportedOperationException("Not supported yet."); ++ } ++ // Purpur end ++ + /** + * Sends the component to the player + * diff --git a/patches/server/0001-Build-changes.patch b/patches/server/0001-Build-changes.patch new file mode 100644 index 0000000..402c2c6 --- /dev/null +++ b/patches/server/0001-Build-changes.patch @@ -0,0 +1,1061 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 23 Feb 2023 07:56:29 -0800 +Subject: [PATCH] Build changes + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 2da91ed6363c0851e4c459188f5e8ef5475e0c97..a404088c7da76e8d05ef506b57fd78345c11b3ce 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -25,7 +25,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider { + // Paper end - configure mockito agent that is needed in newer java versions + + dependencies { +- implementation(project(":paper-api")) ++ implementation(project(":mizi-api")) // Mizi + implementation("ca.spottedleaf:concurrentutil:0.0.2") // Paper - Add ConcurrentUtil dependency + // Paper start + implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+ +@@ -100,14 +100,14 @@ tasks.jar { + val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", +- "Implementation-Title" to "Paper", ++ "Implementation-Title" to "Mizi", // Mizi + "Implementation-Version" to implementationVersion, + "Implementation-Vendor" to date, // Paper +- "Specification-Title" to "Paper", ++ "Specification-Title" to "Mizi", // Mizi + "Specification-Version" to project.version, +- "Specification-Vendor" to "Paper Team", +- "Brand-Id" to "papermc:paper", +- "Brand-Name" to "Paper", ++ "Specification-Vendor" to "Mizi Team", ++ "Brand-Id" to "toffikk:mizi", // Mizi ++ "Brand-Name" to "Mizi", // Mizi + "Build-Number" to (build ?: ""), + "Build-Time" to Instant.now().toString(), + "Git-Branch" to gitBranch, // Paper +@@ -173,7 +173,7 @@ fun TaskContainer.registerRunTask( + name: String, + block: JavaExec.() -> Unit + ): TaskProvider = register(name) { +- group = "paper" ++ group = "paperweight" // Mizi + mainClass.set("org.bukkit.craftbukkit.Main") + standardInput = System.`in` + workingDir = rootProject.layout.projectDirectory +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 8f62879582195d8ae4f64bd23f752fa133b1c973..5502fc8b4754623c600a8a36a2bef27582402933 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -592,7 +592,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); ++ Metrics metrics = new Metrics("Mizi", serverUUID, logFailedRequests, Bukkit.getLogger()); // Mizi - we have our own bstats page + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -606,11 +606,11 @@ public class Metrics { + final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); + if (implVersion != null) { + final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); +- paperVersion = "git-Paper-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); ++ paperVersion = "git-Mizi-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Mizi - we have our own bstats page + } else { + paperVersion = "unknown"; + } +- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); ++ metrics.addCustomChart(new Metrics.SimplePie("mizi_version", () -> paperVersion)); // Mizi - we have our own bstats page + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +index 532306cacd52579cdf37e4aca25887b1ed3ba6a1..964e22ac58824a0b4f9bb361e8f34314274ae192 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -35,7 +35,7 @@ public class PaperVersionFetcher implements VersionFetcher { + private static final Logger LOGGER = LogUtils.getClassLogger(); + private static final int DISTANCE_ERROR = -1; + private static final int DISTANCE_UNKNOWN = -2; +- private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper"; ++ private static final String DOWNLOAD_PAGE = "https://github.com/Toffikk/Mizi/actions"; // Mizi - use github actions as a download page for now + + @Override + public long getCacheTime() { +@@ -49,7 +49,7 @@ public class PaperVersionFetcher implements VersionFetcher { + if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) { + updateMessage = text("You are running a development version without access to version information", color(0xFF5300)); + } else { +- updateMessage = getUpdateStatusMessage("PaperMC/Paper", build); ++ updateMessage = getUpdateStatusMessage("Toffikk/Mizi", build); // Mizi + } + final @Nullable Component history = this.getHistory(); + +@@ -59,22 +59,23 @@ public class PaperVersionFetcher implements VersionFetcher { + private static Component getUpdateStatusMessage(final String repo, final ServerBuildInfo build) { + int distance = DISTANCE_ERROR; + +- final OptionalInt buildNumber = build.buildNumber(); ++ // Mizi - get rid of jenkins api ++ /*final OptionalInt buildNumber = build.buildNumber(); + if (buildNumber.isPresent()) { + distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt()); + } else { ++ */ + final Optional gitBranch = build.gitBranch(); + final Optional gitCommit = build.gitCommit(); + if (gitBranch.isPresent() && gitCommit.isPresent()) { + distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get()); + } +- } + + return switch (distance) { +- case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW); +- case 0 -> text("You are running the latest version", NamedTextColor.GREEN); +- case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW); +- default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) ++ case DISTANCE_ERROR -> text("* Error obtaining version information", NamedTextColor.YELLOW); ++ case 0 -> text("* You are running the latest version", NamedTextColor.GREEN); ++ case DISTANCE_UNKNOWN -> text("* Unknown version", NamedTextColor.YELLOW); ++ default -> text("* You are " + distance + " version(s) behind", NamedTextColor.YELLOW) + .append(Component.newline()) + .append(text("Download the new version at: ") + .append(text(DOWNLOAD_PAGE, NamedTextColor.GOLD) +@@ -82,11 +83,12 @@ public class PaperVersionFetcher implements VersionFetcher { + .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE)))); + }; + } +- ++// Mizi start - we dont have jenkins ++/* + private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) { + try { + try (final BufferedReader reader = Resources.asCharSource( +- URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(), ++ URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(), // Mizi + Charsets.UTF_8 + ).openBufferedStream()) { + final JsonObject json = new Gson().fromJson(reader, JsonObject.class); +@@ -105,6 +107,8 @@ public class PaperVersionFetcher implements VersionFetcher { + return DISTANCE_ERROR; + } + } ++*/ ++// Mizi end + + // Contributed by Techcable in GH-65 + private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { +diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +index 6ee39b534b8d992655bc0cef3c299d12cbae0034..94ba73ecc493386c35c20e8dd35560f4c858b495 100644 +--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java ++++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java +@@ -20,7 +20,7 @@ public final class PaperConsole extends SimpleTerminalConsole { + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + builder +- .appName("Paper") ++ .appName("Mizi") // Mizi + .variable(LineReader.HISTORY_FILE, java.nio.file.Paths.get(".console_history")) + .completer(new ConsoleCommandCompleter(this.server)) + .option(LineReader.Option.COMPLETE_IN_WORD, true); +diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +index 790bad0494454ca12ee152e3de6da3da634d9b20..89834246f5f9c70ae107a19ba5ca451b51f0b539 100644 +--- a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java ++++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +@@ -31,6 +31,7 @@ public record ServerBuildInfoImpl( + private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit"; + + private static final String BRAND_PAPER_NAME = "Paper"; ++ private static final String BRAND_MIZI_NAME = "Mizi"; + + private static final String BUILD_DEV = "DEV"; + +@@ -42,9 +43,9 @@ public record ServerBuildInfoImpl( + this( + getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID) + .map(Key::key) +- .orElse(BRAND_PAPER_ID), ++ .orElse(Key.key("toffikk", "mizi")), // Mizi + getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME) +- .orElse(BRAND_PAPER_NAME), ++ .orElse(BRAND_MIZI_NAME), // Mizi + SharedConstants.getCurrentVersion().getId(), + SharedConstants.getCurrentVersion().getName(), + getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER) +diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +index 774556a62eb240da42e84db4502e2ed43495be17..06aee16a32deb200d7d6070ea8bd018a8f4a7b52 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +@@ -11,7 +11,7 @@ public final class Versioning { + public static String getBukkitVersion() { + String result = "Unknown-Version"; + +- InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/dev.mizi/mizi-api/pom.properties"); // Mizi + Properties properties = new Properties(); + + if (stream != null) { +diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png +index 8b924977b7886df9ab8790b1e4ff9b1c04a2af45..005f6f417598e6692d8e349a7dde7f96d44898e7 100644 +GIT binary patch +literal 27503 +zcmV(}K+wO5P)gW-b-f%0t*Ifcm1Tn{inkIz}El&{st&A`Mb^rR%1v&Yw4$& +z2suUmuE+-h83!CJ{<6&f#o7f=UHrGr5CRPT|Mvc&zXuE&0vmG%U~B%W#Rp1O{=L!( +zKTHP}CjId0`1bGmvB=a;X7{(t{`KbtZg>V;X95Q({`v9y^XLX6a0q0f?W3IYw8;h= +zX9q7g2t-x?%-;qUI|*oR{`Kaq+s%&s1viqrCL-?Blb@;iR|u-qHjQD+LKU{GPn>ztJuyHS@2;6$lgG +zld9*RmEfGQ1r~e=SDhwiov3Vg9uyx2Aw$HCocP1l7g>kxoV77+ps7V#L^wgwps`Cw +zYwoP03M@|*DL{LPuLNbS2P9c54->aVa +z`RCfCnF$V0iFbSk9)9fV=HiK==HcDty3N;pldE%tYj}}geW!+FZp@XNr-qXYBvQ{s +zV760djBS8iX@u2WbR9883UYnDroRzJNRWGxGcQulrNaHv=5<_Zh=hotU~NZ8Q5I5k +zp`M-yZg9W5wfoA|!)}BK5@SC*S3p*5-PzN>UvzO$T+YkFxuc{LU2H{Sfm=XK)VIj- +z&BI$ +zV79WdEJ$iqQBL8))R&8*j59^;%ii|q+Udi&;J2*1iGZ?ucMsDul1y34t*uueE!u2{ +zWoKq=FgiIIBOz~&Nra*GyR>zdrR(jJCWky8UOB9+av!oeE;`8*001BWNklxHOn2aV)ENYl>fE)!}f2Wy>yWEW+Z05h=M3>4jM@Aso@*tL>4 +zONKt#vSd->@5A?f-}ip%puC{l1i2T)8x9hEI!jPKIJ1xV#jn<*D$nzV0eA4gKzClK +zhGEFE7;3!bJ4vxDOTsUU-cIfh_%xy8M*$(=yA#L!hKWE_)pp%-#?54S!{iW#DawPR +z)6#D=(GW@oTC@n+NI-FLChv%L6aCLI +z%&#rK2oD@Sp(J}h8Pa@6S2TG*q;D2Pnm;lyx}Z|*r(+K%f();4sHUMfnyx8w3c^6o +zRN#P`8BH;eE&zc@n#{yV3S1jGD!mSY#fEA`f4QvCjkXy&-97M->)DggdV<4F~I(jA}ItjF7)y|YN +zmg_>T@K#1d7!D6Lgj}p?NBbL{K++aOqOhB{7Ji6>I2s7~e4izETZea1$XM+9=b!b6 +z9B-hOkwhk|W-ECmIpni*4_ZG@2vw*lFZ2ykQD6kOXR+@|sN>4CysHpVv8XLZooGar@#i!vPG@FueY47^ZEQ +zXr-o@ad|$Wy4BTHSK$;uDVs9aG!3jRAgPsA{RX%&)dnP+U0l;h%Jbtk+3?N3Zu5h{ +z%0)f=_GPFI1RIZA<2Gh*k#NS9H^nNTuR(3CB#Q>lwzfk{$>gUv-_c>tXh= +zOvsMSrKP2(F`B{#O}7Erc-?7)#lqFXL+j^yf=NeIMG>KR;|QNjl?@Io7CBZKI1INC +zi3Cnp;)#S_E5?i4HD=J^TOg1u&WScIP;EeB^ZBLh1|cBT9N(ihsHDkeiYn<0lE}aXz~=4+_{DUXJEi +zBx>g`IYafyxY%fY0mGf`QV85tl9 +z)CpBCk>JYE2sv+!37b!dR$(;&Nr(pm#TqL5T%nkY7ptuzf*<*( +zmXM?~o7u&Ai50x%;X#5r2=hv<1$GStEznwcWchFJ_9l}$BQs1CjMlcBHjac_-r1?w +zM7;ArVWN4lS_Otku>yDXS}_tSmLMzf?I!O@3+_dflH_$sU=Mf~*|C(L+uZu%XYXD3 +zawWSt1tXn6BAcdZit>6`tHTgpx2F$|(FYd~Pgztpti(!^AZXz$4?g?r<^GUg79AMQ +zEnqNBhZm8)cm*ETV6JTiha9V%t`_4t&=Y{f>+wjWkPDp7NL{{8N??DAc`Z!pjH(Av}_o?_UK$XwBHD!eiguxcqK!P&dq` +zWdVYrV`<4yBTy%>iw#X%y15U8OKcYkg;FIKsG#3SC0;7V$Nj*Ra6M^o}A< +z3_(Wd1JYW1e`|9t4>;NU@8|O0&z+GpNs$C%1cg +zWCuf-A`|j6jAvQMwR9+SgUQqw)8O|=6t$Kv7E2HjDDfNsRZ6*dwUAy-*P@okp`fFR +z35R(i5$u6&Ne2z9_I(UAn{!Kf1hn;Zer<|HIH;4gE--Zj5NYpKDIELz{Xh4bP1kBV +zB2)(i$_Dy_VUS}P$**UV#;&wD*mv0JIuv%X7%xPM)oQ6yDFt$sLOQ*@JsQp6bSPL` +z1(%l>!U9R`pB*~{qz71pG(L6a>DI~$IPp?;X>oHCTtLz^;2|i|D-~b@pq2}?DR4Lx +z=qWUceAP1yC1FPWjGt)**41t!5e>(`knua-zy)Sx+b~I`)75IN><+-j{o9#4|0x4a +zyjUs}w(EL1p?CpPeRYTDQ)5se2SCQh$0kbH0s*5XP +z4oL#??kLm7YiWE*I>*+(irQAnVf_A!K_(RH@9#$-Izo}Xi~vXfZx;M$2KGQW*ndDm +z+=0n~$;s}4$v>Vt{?0pZpMN$p3;Xz;VqL8zP%l9@N8ABBHn4B)2f26e9zyX11jZxz +z=Cb+x)(WuP!f|zR<)belIh`?-9rR|XYP?bT0SzsjMBj?6-=0aO45rr)1i@ek2hNBG +z0tJFvVzPfB1zRic8!XseHF>^i{bTadz(DuFr+1$Hw(G=+KmGIg@n3&?r+czlt;uzp +z^D@Fa&`O$)+|UGepKF4&U|Z$$QFP0qD1neRma?0Ywl@EGa}i)}-n#kd(anE7Sby@( +z^&u_X84hD21OgUKVv+%6Ca$v)7?E;uIr;I-pdH@I9#=yl<&p_mL`8t +zBe+hGUO+z35~atBS69|9yaxq8_0i(3nU(wx{xz+ST72Tal$v2k8)+KHPZM+8&_F(z!@~?2@Io90B7`AkGJdGwP&~v> +zGifB|oGwY{()lnYA=osu5Uw3kRyHLpw!6_#kYsvB)~!o$+uQU@!}csk*DlnRg8g!b +zrDrP#e(HU?=Y1zy)4J^4OJY+4#h=gffBygHO%FBVKiKdL8e#YGyk+^7)onfer5+uK +zELX2S23hoa!-1*q?_J$$Y;C=G@uG2kbF;d;Tdh_BDm(w_rzv!(lSzm`2^VujV +zsT8ynLBNwiUqI>b)f7DLE8sxQrBGth?#BmYQYb`{8CA>y!`Q;Y*jVM(X(u|1i_VGD +z&o03C3|BMNk3f(mLcE-fd-4~1+#MQe-=cJYKzsf3FE{Vr4tT9LcUK_ny?yW1*7Zi? +zMWb;aPVxFoH5w%dmK-fW) +zypDeJ00Pf9!%h>#;hb>%Yhi3)L33#$R3whN3(>4DnQH$fkghYmf4Fk*KR54v6Y$#L +z9ZO*7lk=C?8=ITg@5{yF3_c1-mKOwp=LPtKvBxqBV;{r{XGxld2TkHbQ9N@rfw4>? +zgs$2>M<4~em{)aN5NItZh{FVNIJ$$u(LMNH%cbeAI4+3~dt%M!c@j7Y#L@wR@=0&t +z_T8Ip6c7(IS@=0(sm>HJN_7Spkc#1lgdU1yA`HXX3Gxxm@W}+tt$xqf;S(!j>*+bp +zf+n%o`}ohF-Msfa4DlY`1&as_1s>jMR1-o8K;T4C_fQPM3b7JT@lb@QTu*75A&PZL +zQe{<=q&nb;d07GB*wIb`N3rWM`yq(SKd9=e{&w3e2t3X$j&_?M-A(}cEm(^t>qZP> +zf83Wrb#d*qd>(!1V-T3YJ+@8|JdC})m)EyGgQ^62&bTcY4_GcOXA>-=2tqUpV&ExO +zD9Qqrrqi;hmCGqviPALgD;8P|T8pJhvYHpA62W2;jyYbC!l2qxQrb2u2tv8=W+Zr6 +z5qJcH-Fp{uE%>J)!vMsNnI2nlfBj;Y^`naP{{9rW_7RAg-C?_f0et!D*4EwcLGaLh +zJr>x)x&ptxcNG&G$aA4Kp-C{ +zcr<&lp92eMgMn!PCRtKd0nEg%CJCht;u=)7BCBifu+9f6V;9~nc7Q-t&Mp9m-c0%d +z!M6ks4-0%g*{@vkegtB%dI9786)<{XeiVb069|9%`PSyv-G{w_u%#EV*lZRUWaqOq +z&&Vn}UMy>3iGBbMh%ChPanLomBI+*aEigma4?#{s3B4gH=In~V_)?pSP;BM~a$^G( +zr1yoh!QdeXKp?S9>@>Ab}5fhFnr%U7G5Tk8*} +z!k+|sQKYfjPI>>d%u5O>m&@}}nq@%w6zVjFGN&#JIw)jhh%5@Y3&%6aDj9Cj9)Zz| +z{GHL!!VtE)db~sDS7cT;wCtnTX*`H2o+VlGQKBg4Ypsoq)z!H#XPd3O +z6ytb!2Z|>vXasXSE(k30tke|19GmqV76@>>IXfH-jvNPp)OBuqKULRsSa;*Dr45`@ +zVzn5|U(5el5j>fpm!^JLZ|weU_aEQ=_2G?+kgOV2*)U`pmU00KfkqUU$+xuT?A(`g +z=uEHGb|MVRvJ6WLqAq3*lY9h`*cC!*DqMlv7wgzUP-C4N84Wr{Mmj1IJo`@9^P1rS +zt-E&SmOvx1elg=Oe*p5oR0R2m+Xi7}>eBMc_16y`?B17TPAzLHO;XW84@=W&CAqOW +zI}6KdXmWBA-AEnma4a)Ozyzab#ByC=3G9fF813k0OCf>7USRU6w-wVva+N*DXz)Xj +z?mv#~y)}~BegyZ_+yM^rqUskvXsr`Y+fmvey=@$vkpn|RQ#YPGF(`JaWk@tDM8OA1 +zK@~B?^z_=bNp!witHnJ83rU`jKB|@t2~-3D7^;Dz3d)V7*yOna1KDK1olam40pD(N3{DPJdBps)km8o^mB;Lrbn$ykDyKt5Y3|)v-d@Y$t+X2uu6Up~rT{ +z8~*(D=5i{Uu148(5mZ>u*4C~8fO$f58#Ruk5(yGUF_A86Vw48sL}>=}LdlY>Dx5-# +z8X+hGrPa6Jg4U58R`!M+5U;w;p?jRfxqv`2{Rm_=bl{K0eR9edJMTRir0e#n6G7ZZ +z(m@w+*6{hirwy$LZb%MFWkW0xOVj9PFmwoVP@vdBJ6=dAvaS_ribKX|M}tesXJtth +z1llOG99Lk){q2GMiaE3vzHpqz93Pxv$Hd;hL60U#?I4sX#N)E)bKU3wvEhUI%66=| +ztZuZ%y3vo0vx^nkJiK&Ue)Dou(xfy|Iuu$0v +zo{kziP4X-S@t!1$DaczzNrHSy8fBXEL|94Nu2l91%z`{~;Mn^S3fTL(rmEv**Ok|~ +zjU|6P9us|$O9A)$i`+Jd4g32c$CYooY&bNcdlkrA?k;@$i2l#T(4Eb4c_x}j$YmoH +zrOEz-wlvU(8hu|)FM$YPP7!tplyp&(Xb4`Q50a9~Vm47sNixgx2~DKI5ACHUcn3%X +zZ6O11oFA(E5Ttv=X-J}mGKy=-c$8^D5bn>%xE}(?ZrzVSOqBO}K_+>V!Nd32% +zpfOT%vGrK0tbZC*dS3l=-QI`p)s}98wJ7Yjdw=7f+O6yl+J+Qi9^dzQpXYghW3|p( +zTs9KA3JSn*u23^+<5)X5U>tXGWV2rOXCR2b#SaugBGUi&0$|8_KuQaYbc}_;1)6@L +zZLbjv{CN51g+QC~d?3Ub91;OX$MoV1Jb%aL^f#1T1Rl~veapw#h}9NNj@v!ZUCJ?` +zovtAy4XGOM!5TknCOt5oq`1}0jROLiUPS^7&S8#lTm<+I+6n5zsPt7Bm)mN&axpJ# +zPJrn~-N5-kN{n=tJhSXLse!H{af^$a +zpSQDtX7I-PW=+^-$Mj%|HLVar{Qh{}f2HLj@+L?_&fz*Zd87JgP(%deLYsFPw>rDP +zs|-sY*MAA5u(ev9vmxE)inkuNq5NYNR(15b(VfZQi?Lxm7(Omd34;cnk&=OI9nHrF`7 +z;Ns0-kEM`PFrUkyLECJD1{4JO#QY!-z5jhdS!q+Pv81d_`vpTlAqR{xr;CO0ThZz* +zcbSJFWVM|Ea`Z0B>HIxwy5!Rx$gKH)^xlB8;IiHCZmmc*=X= +zb`vI(a4}Q}kyr#ja{UjynX!7vxXW&~vlbY@!U(`IOANB3OexbFSWdtZ2Md33@WXwY +zRwxv-TCJ!p{S1m$Ycy)L18Uxx7l7Ek*QO%NFu)pzWfM2P1S0<2J5r0A)X_I+c`Y#& +z&Frp4AElE~Tf`oTM6CEQupCBJ!}8v~+bu@#R1{ST?*)j2HxBE0vjI1UU7RNpvgkF) +zU!hTk#W@eoDA69685iVyD+h#!iB(s&wEg<^>umPv62fxg;I%Yer>-m2{#X4WkQZ9Z +z-hFKUa4kkNNfWOt`BeVq?apkOl(7 +z_uWJh*cJ(t4P1;EEnnd0E~RG~e4cZKM>#}sVe>16DIDi8v__Q~l$Vw4AWYpBf4+aq +ze`Rnzn~3qzXq4yELqpkzCoESe9xgq5wlPvmE7rMC0ByF~yyeSNchObcaMd_<9*~m* +zoi4t*+II@10~DU<`0mB?bfz=Y84iadOx4F4u<1cm4XVnkFax8Dw)wR5|OR` +z!Iia@y$OE%+0w|vveMexI=Z$_q0s4s$yGb@@+CA|_WSJ1@A*U`5CUgX?Q~5AC%YG9 +z0O@k*#G?Efh*IV=M%HGIhT)t +zR0a9h6R}wAxK+cjVG@-?#i($HX2IJPp3CR@2XntbydBVKwO|;*`-0m7Bqsu2Vl2}Z +zz^SdPqv;QU1yqqhJUZTr$yp)l*#L7zI)Nrvy)DDW)P6JhHK@^ +zD~A*`RV8ACTqH3`0L86WiS>80Bn~|_%emsCR;diPdOTou!y3$hGc1E4PpmQ+7{|MX +z2a_MIb65QSJov2lWnY(+YT=PV)?wCu4X5B__luOiKp4Vx>!>2tZJ}KJLD0KbCFJ(c +zIa2%ygGl&%b@h)&>JEGX!_^BodLBLQd6aq7X=`0>2zvJ^VYh|a&SVl%6w!V!J8C9L +zT&cvFm?JjsARPt>>mFN_3zKNk7-2^-T<9wUqbEb&@|tC!80*gH?%uqyQQF5_-wf3Yp!>&bhjSci%#^YP=Ajiq5Tw_^UpFB{QpnZGiAwRmM8*N;3PnJfQo}}*9Es^c#K9{94476bO_v2Z +zdkBmw+`wQVFJ$2ZUp~H-&utC$3+m5iShXVz&4IpAhZ7%h!6VT +zHk5aFRYOGrRN=W55pQl5A!qf|zl2{nlwY>F?m|>;RC~g@(6>gsB)+LM*>} +zgZTC_KT3j$(yJhZNL5PA9!q9OMlX|*);Q+_mnhZ9RL!jhjj%Tq!)W2cOdd%<}KL&ve2t<&Hq}dA2-D3$^ +z93*@~s}rq-4D%2JFn)!cTeDu2Bia#02QCDh{J_BJK*q14VvA|9} +zZ+bY~I(z^E828&}bfQB{0(I$r8f{-+Z^)G#&CQK-K+cohw981TM%H{&Pc}eF!jYhU +zJ$iy(-^i7ay!os5;Cg>`d(jbmGP3#YJC-kq+iV{AvRRd2VSKMQzaS2x-FYVJ>PuH0#sS>FF*wyGAp#ZoYaC-S69)MuQZ!IQsVQ +z%TC3d*Aoc30?uG4_&MSQ)MmUzu;U;y6Fw|3sftQi?!k!7gX~Q>A79SCKsy +zHY4-pi@z?=>ou*zt$RMw^=!qL^lJhrQ1^EoZijT6Dm@WH!YjGyCf%G8dowRKG0cUW +z!bzhQ(P-Xk-jeRk*N{!om2&JKVt){;rXo&j5DNz!VVkwM*c%dCT)6cBB*PjK+mu$X +z%WAWVgGzlxNo|Y$FBcyNT=kvGetk!`HVL(@*a;vAg8&i&qR~r(;Yu&PCloiWmCHA{ +zkl}Aw`iC@RkBTHo2^~%ojx@W}?$)?VXG?_H^o&O$<(1Bo!q{Iy4pNf+sHyx!8>%<6 +zu5H=8xaAmzVLHJP41H5jbAq`@!0E>`PVSo)3!+vaWcB*|P^3h>qopl1E%g^aExS6? +z^^wK;Mo;z}>#h=HJml&+P9Oc56xW6AG_8l_4~4v}aG%}Icf +zH1ybq+(Y$tDK9OLo_lJh@5DLMgn5!Q;cU}CthqDbTio6r)OY;0n?m&kJLz)}2Gom1FM4|o +zy}gNwfE;q}e=J??DkxD@Jt{Po2E$6@q{Tr{7OC%^){oqA3nl2aR47}Tnc-yb$AY8j +z8u#N5@c(yqc3!uEZN1*|I)jSCm@Z6de`20+001BWNkltY0E!Q +zMCte7%hd|OVGL6!j5FqrTV;#Kj$p?=Y-}vm{y~jEAY=US3lNYaCH2oQ484Xujy;g; +zT?-=754WK{nMAc#4_XyKAhf~6b~Vs$15aNnv(IYWB#o$%sm66PZj=RMBofVfSfG$A +z;AX!whFVD1I+bGF8Jfd9xYFwJ2OLIBH>ble2d<1q-(;3j$@fGsM1lF7#$)<20KpW# +z(uQ|i{f%mM4^kwR&5VOE2`1BY{?ge#L+l`u;VX2|x_)~?@zbl7Nfdv9qkRcDNGZvo +zHy$|f?h7^9LZOh?t=G(!$TTF>QxXnuZAy!cPfPMR5Zi!)Lvq%AB5=R&nap$G?^z(p +z`+zmy<-v^>T+Cm^asQkEGukZLaDdNBF4*s9iSuoP80By{f?A!8s&45pryH@bW3zr^ +ztE5b=?x2!V;%&uWF`39U4k3)XIzyeo5VtG{TCFd?|N8pM3QCBm)aq7s_n^;9fEQ%* +z(Edro+R#!v0vzohX@+aMFfA`#=zc{Bs^PMmH@#FZMLi%un-LHMgmxlnyF3elw7u_i +z=6fJUIh4hCgz~}LZVY(vpwpXCkN_g)hm9VbFAh;hC~T*))||GD9jo4cuitDbYgAWI +zNe7(<0Qo&5#dI2B)O|G2BnFzKwRL#m;X-Q{9e-EQK27`m%AP8;-_W4C-#0O}r&gcqdZ^vm!GqePEYTq2C5XtG0DEg>TD#6v>lw?n~;POiMNAQ`wxhf?;d00JE*%B1p{bx9s-zi%xHGMQZM{cNdwi%Q~7$s|?j9GOK# +zAaJ%0I11@raaMU#R@>|vzU<6@^JU1f?2Qf#-~+o+A3M#8`($MEO`V?zF91C$)kw6%L1e;n1mqlZlfBaF+szX}k1^2Jdvt=hc{JH#R!v +zYtsMqC{LZUmTOXXvg&7jZ7~05DL;nDy;_C5)rGEP3Uz} +zcwJR`r+K}fItLHQjYAEjtW%LWq1fW&3GYu&$)U6+2=2~%uq)I@0&gGtrVx#XnnwMu +z{LaDU<%nf2;-v{t7dIeEaE7V~^B1Kh9REa~>yx +zptrfGsEEN#ErPgZczzInA^P5a2?a00WCBObpqRwop_!O$k$l{4)~nim=eQHV5!MK$ +zvMEufNi-;vO30x>IeL&O-yUnrij9M^PP8@M1(3qRdrcokli!?N^*H&VFY)5y0v79k +zY@2^f6KNX9MQbl7_DFd{D +z)n?Za74~PQ)g{4tc*MsXV3f1`a8p;OU0remZA%6!3 +zPkxs$3ChPu_t9ar@6Monb7KqQ9X%g{%;8)6T4yvGlSVI?PRG|VYY+6!F3fn*S_HrF +zgMUrUJ)Qb~wyF6jXVCH{E_XyvWLB&#b#jI&c50P`&EA1H5DEKX!^eG_Wdt}DG%V6? +z$KE$3?OF(kO;I&>+~p7+NKAaV7PQ*rZT(pJD+XiyT!jLwCsK +zju1M*53fLA@7_?{eE>i_9;3&IK=>(;6b<6w>yifw3yb)3kFM3%as93+cxW{1MAk&z +zuR)L`Id>b*+WzvE_ATI8QL|XI0L)Zno7qvY;A&RssoCyTRkkoQKQXE&K`0pfd9h5@ +zZly-p1a(7|D^oqzvz2T+1?hMrfaHKH;NS51fXCqC=Q_<9!g&;|~qT8f|Zv8Aba3q1lC83derKfLci&uNm$M +ziv2q+LM_JodOY)Kwh#_;si5>v+8PL20 +zBzdXx#A2Qq#PSY=IW_Nb=1oj3>0)7CAM1=NA6CumD5AXs6pU8VfFh*!?^7q>ezAD^REsSI9IvoYQK?#O2}PQb@FzO+O!2 +zm@8}2fN$Sy!Ym6R5C^>E5pmE07~ooa=}E8i6+Z?b9{8GJoC_7gqL29L`x!lwUfS>0 +zjqBRh3U0&|3dvoq!c1XgB8z*T+S(04Ab^7P9kWSu9xl_0oU=7*uJ&X2uPh};Y0ao9 +z{7V5xpePr+`za-gq!F^~es*@^c`1$@4;)KjTD5{UN^;a~${%#m+d>--hoSjs+-afD +z7NCv!lD&p_*cgk^ES@cpqEA06+Mm+H>qUO&nb)mfYwZ+9TzODKfG&`DhX&+kRTcs!M=I35&sPX5K@0*Ra|aTl@BGL1fJqyBX$;Ur>sLJ@mNraVCGfq%T*hkR>=2RF69?HP-} +z-iN>ljG7!%Dauu2vx|#&zIje5N6;S;B$Pohs)A^4qa@d}730GcPEgkSVo`R>8nZ7n +zXmR?2P2+=s)G-NkfwKjZGcdy`)Xhy$F~Vnueklrg$vWu)yGQ- +z!iFgcDQSHXOWc4~gpL;S%W$n~^f>(T#FU>a5L8&4fsn&ziC7Fyf3~?BfDx|F~kU!3JUD`1S*dH5h*Ns*R1NjySOe0ylol+*@gOFIHy^{3+vL@gdR%*hEfOWjOZp +zaT|`RCC;{*ehGJzIGg-3kuMUg7K9)W;&G?8!{?k3bIqNG&0|EgEG3p#UXTN2-()IA +z_8jKVLFJ(JU_dlMYs)7nMyjs-49^s2LK1e6l*!+p8L_{-ih~BO|Z4_{=AVpu*d0+EqjMs1Y2qD*-#louB=b +z9I;d1j+;axk-5$#cUpYD=+QM_V2~pclx>2Pq--sgDkGa9JNNG?EZPDw#(-qrV{~gf +zxy{Z92SB*!A)simtVf@NZGnJe;kc%u@9y7WX4Q9E1A<5qO5;M?l@ZQ5R%nK9fnSh +zsDK+{ihSy%T8TXVN=K8t14;jszX^gQiG^eHMq_uAL{uMal?^)m+%k|PVyWn@*}VyZ +z?KtxgSKjJ7sexF3`?g`F5w$B!N-;bOk*Q; +zeZ0Nq5OkdcX15W$Cm~~fXWs=?uofn{;)0+PVjK#(cq1U+;1qBRIR2Dp84L!O&lL;? +z*IpR6D{?q}58~jb1c998d9m&VMI@ru4sB;$yp=1FWR>|Xb&BmED;l=6ukUm0R>Lwh +zC~Ayg6oFx3SI}cUET*X>h@77)tEz?`R~HWq00nZcjEsT2gVn>{^_pGKKLfB$jq7{4 +zi6=b0btqiGk>#7+e|Gtt(D)i;3b^-6VzF2iUP3Lr13ia}PSrvta7{3MBb|QJ)A=71 +z!rZX15NaU;F(%iIFCQh1-StAw(RyutbG(%!l$1ds@pHP+{FGjS$9DY4(lnfeLHz#6 +z@{xuWHjxqpk=TLcN5*HM2qh~|Az6RwhG$B +z!;gNL*;(}U-24oqh;a^z;hgTp8L!I~N;=ExqS1USiDnfH2&rBNc1i__*lnD|-3M5~A_QlN+k_9@Y{q$JdvwGvg!MuSyqz7`YjN<9riFQ3tDn+Vc +z#4YloD&I|!Uy8W}0<*d4ys0YgLTNTs+X61OX0=-HJ-5Dj{rdHPU%!c?^R=On!%Uon#%Gx&aktKJS-)VVzblIXcf9`1DW1Rr6jZ}rEP!~+RlPtTW!EX@5pn5 +z4Y)1|Gz#vTY4E{AN;^Czg1~fQ!2+WZ>Ij;klQ6=J4(=KeQ4_$y#Q*kp&MmIV&Y_gh +z_K)+q-{13|a|-UB4T3~r1PwO`6O-@*YIV`}{(S|%j@V5knoc$$>EtI54Bto=0_4@t7>#jHD +zYnw=)jg8InMcFDgEmXZ5B@!bzJEVFjP}*J%9=$)=KCEXs4ON8G!CEs&0c3ya*R^M6 +z6R7S8Mo5iD1Jl;zfo0W1i7^5^#b99oX~(H=|Me3(hS+rdM$`IyfTX-b5l6ZY9B_Tx +zHg0hT0y>=r8P^VbwSQDFsLJ3I&W?)&Pp|JAZf{FBR-OibUzZ&^ +zR9Cm~eqo`rvvXje^Zo2CHYkD{ku2lm$33wOM%(JrD#Yd0?)ACaw<~&TDMTZ4fp>|mAa +ziO=`sZ!djP`}N2^?gtR1Qp{Dx+(d#<99$%dMy=Kq{}_=G|o1+u$q;VLbBLLy7S?V(&EW28`t-9vvkxv~PGVa?33q3uO_soufLM<7RL +z`UVrA*xmv`8fY#Y%)=hCH7a%B)B%M~r&j+74DFNn%RjVs-*J%#f*_DV6qb>;ITw-$ +zeibhDsRzW=+=#!I=X4l&g*?NWUO$8HoDB7iMLuIM$y4MXKtknd54$K53S@WP8-aj! +z@*X=>4)39D0-(rJ+tCzR%Vx9p+;D2+u#;oZ8;EMX)28R3QyZnW>FKJEL6l-2_b`B5 +z(1^D{xSC3w@E}Afzyq$;154%~Gu0%iPy{Ut46DRHY5_5H5Wz^dB7I_NYGhWG!LvD> +ztc+@NxmneRR;LQ&R6^CGnma_S^ocb!U|Ba~#J(ViuU?0P +zEEFLmNd^L7V(=yxrhJV80S_T9x0YR`+pU9F18#R<*Td@d@oFi*!6D`BC!GC+iYjyA +zTF&Qw!#1MLGIx8j@Qt3>&?^$gtKGq~(q-67-dF_!D-uO$T!N3%0YvC&6$pl*DQ+*? +znL5LVpmjcir7x9EX5=Ff+mUH|{Qp3d2n4br2qFe`s2`0Eas&b)5l9@Em`hz>siPD@ +zEDAEPt8G^R85IODsOVicrc9$RU-As=ew9j9+@C>k`i5{>*tn6ayv;Vkb*8T_-i-}{ +zT#;Ss`bd(xfeHZ{Aj)HzY>7^n7`5741vJ5iH?=6W9eT=g!ffcsh+r8Sc;$9J20899 +zS_ +z`lCm9MIvGMJQvWuV6FfNQeOgt4@$#sA6 +zlOepM?rqkc_T9~rqeX&PdT8h>gKnh56{n}X!lu;)D0Q9hXjZhuTku=!^8#AQqETi< +zatN4$+Qy>Pq4u3Q0AjG~SyGnSQGLFjKp=*Q(({k-y~0yadWVRVzWRS>AIw8~_=1)N +z-T>aA2nML+AP=DF)W<9qOM1G+6@7ns43{e?u_RkYRfT7nVId)5VPVnHVM#RWsQ)d# +z^PW1r`A-1RGVo4hw8DAMBQv6nGg)ou$oC<%!@uq%(`vOcX{{bMo9*eQF-uOC(3pV1 +zw4TesAUn@B0EmNyw&E3M^qUg*Y(HWnY|iKLno|gb3m_t~=0x_>niwB8EIa^2@ev3$ +z(A9^3kn2ZAMxQ-;HBzmkf>?53Y?)_ghEQW~WLS6-lQx1~q;Y1hqULnwJuN+y0%6EJ +ziynzl4#|T4stX?+LCFw#I`bBkE?O;{-HJrTE+eQYLig<<9o3c+SP_mC+z-Mq0*UzB +znejhp>Y=BFMjSw54g<(xu1}e(3=Xd1|3ScxKN0;yx{S! +z(!xG~goTG&1#8}%9PjW(LVVuo9gFOglu&9PmXYqc-6NBS#!6(XI2)F|AE#N7AdlG+ +z#39jo3Ya*A!NWS7vlAK@0I^980|P86L3Ef1n*)PvKaxKl&&4HH)Tt1WFRCW{X`aUC +zQ+^mRir-TNfuPg^$uKbga+H~r6doC!Zn5apW7T|42{Q;18G=B-bJx5@n-$+>it|n% +zyTxY220>ClliNK)IUO)J+v&-ZEWm)Ys9Y{r%%@FZj0zAKbV=yK#GkU-$G0%n1c +ztwsB0*+MOQ5n=2V2*PQDZ&tkcJCSOoX8WL-#pgptBnbTgflMnuWAuwpzV99{*$2-4 +z7qKSqVBSrx2q+K;demc(|1xs+K~3am9N*qqvq?-AJBr;uvIM*fB#;R+gqOHCO=1WV +zO3Vwyj$lKE3<2>dFG&Z&OOIM*4wPeHOiBj?bhJ32)jQ?@1%U}Cwdfowa7cZ*Q}C*m +zbMD%D)BCT_Z?m9iyy?S+WH$NZ_sREp-Ry4sdXR01qaHkK+lieSpI+=v%W|p#gv-HQ +zZoTyEs%NprAvoMJ*I+_=d22}tF-F&gJeJOh~LfX!NM}p3F>M +z19|H$yH>Lp(4a#;H@|CiAvY4bAU;?OkmOO&w}fTIohy}1pET{*abDV=2lWtvRESi{ +zhBhU*ma?Jr$k=BW#RghqI08i+zj +zBxN_00(T=>BzeIh;q$p9sYrrxz+}{o=L-W=_*-j=WF~9v@~cZH<@S_QRgL#iO&9in +zV==CU&dP?o#U;_+@9%Bztf{O#zvJj-Lq%SPQo!RW9ob5is6Sf@>9?)#a8=UVwfQ5L +zhRQ$@j}gjxLV_$|%{D?;62BHBYlgBG%3kqK|KV=tRs9vGnjA5xlr50M`0IQN14Xz= +z?VHlAAGC5HlI_8tpqP_KVdB_oJ{351oVqn%UG0J;lZ%uF60bZA(v~@sy2~o^ubSqW8w=J(%gs%K;%Z^p?=!Sx}1JZ2&$X@uW5|g}nWmtmAYP619W!wXSpwQpNMBfyW+(DE>%hWLIv>(2nrOuZdU?;@P#gWN*KEJOtAY!V_DePg3uq+&>4noSEYSk7o|7m`kiF2Pb{hhTUvH@8LQPkE{FQ2GPoRO1KLPnT +zqtPuEK!37_{~8F+!IFM5Zq()AT;UQ0I%uw5)vW&5nYqGS>;hu<%a +z6^jL;{@wzb7TIh{eMiT|0p=LPz!wWx^sMy*qbW$ir~=P0R%K;D0e@j>S!~3lp$Pxf +z>qO6B&=UbbE5FoVRI0>oD2E|9FpP)>kY=1vkc&_QwOa$p%s_%hC(Ws>!Hu^vHD} +z?_@b~3Bg^rByP~)%Oz)?G~yf%UjqB^BCnFpm3ELEjwBQbZqezCwKzd2gjHsHvSu*| +zqSaEzAn}42QOsjrQSR|Qu+)}kD6sOJFGho&6({Ti5JV)aiVdS3nZ8x^XTafpA#EIIKJpzFIg9oiA##I9vjUoK=u( +z=z9|gi9pH$gv%A)rNYgDIWs!PL`#jX>VZgxr?z-}IR`yHpJzakwER~W+r+aV+f!@4 +zX|bI?GCFc8m*y$;wtJQ5r8FI*%5_Vp7?53mSir10qtB8M1c_uenQSnt8y5P?n)r-y +zC4{gqPk{&bDTLGL=L0FYngkE!T3C+Gh(P$Dp5mHW6PTkQ&cmsRmpGyps)k#?Y9@&M +z4Yl@am!!!VZHVR^YB8`Jq9$t;Uid2z<6GMZUC1A&xt) +z>X$%s69+`TzhR07*naR4hastH`t4+tK|Xx}ILQwK@E9bI>sjo-TWT`8S=7==5Rr? +z4Cy$4Y(ekhxB|*vARtc;r}zJ)r>(@*x)SuE)h#}=b8qfz%DA(Al}yU8MJyVXCY +zh<|mg#mjC0h!V0dKQsu(10Wd;Cis?c;#5E$PR%t&v2o6jv^$NSe3ImH)aoLVtJuR| +z&R{x5csyI_TDb{vqP=d*?GMPm45vm%3`#G3Z}*|fQYo#^o%GLJ(l@*2&P0GBR*x)5 +z!P3AhQGZez87aUQ`=URRF +zl8x!!(`V9x +zT)q2#^xF}4?nVqkum?e2P=KXpI0-KPCXkVftFA@PczdO^)E~#i6$%W0y=lw`kc0U+ +zOGylDSfWneX9^)pQe8dVY%M36t-jd@zMuVa){;R;lG>-{%M=R5eB%;j&(y9I|1<1} +zZL;F3{CLXo6l(fxUYqFOf7yMg>GEket-R4Qx^yi19R_544TQz{MM63FmgIkQoPAIe +z`5DKz{^9PLx=RZ(?zE(UPPlL~3nM~ACNFSDWif_L94#vinv`5dM=z0F30$a=N@pNu +zoZRrH!0~c$*HO6QF$!}yhMsM_IYBrmbr>Ap%6Zq;-gu+qUHey`-|hzAFd;8wvLT<} +z_jzA-Uj?#I#>!um8V$T%_!hK>PAA3S>r}r`YMt<85wz3;EBJ8D?a^lgR&w%1WzOlXeri_mbar*sF8tPGfewvUsA!$(E(9+}bp*W$Ks1I!pok2D +z=EsX^bCVT`zb@T+_F`#iL&HOFP2Bctdv`qx@(hd?iWCfgg(8HjFCr~JPW2DA-Avq5 +zIMp>8xL?n(PxFnq2nql(8bi2fTJRDZI^kBiBGR`MjSCDb6gTrdZ-WfWvXE4DilHHz +zo~<4kfx>v?Gi7$}(e`aE*0$p}`+L$1kb`=x%Eq?B-H&ogFQQ|HAI|cEI5ClYMEjy5 +z*N|9 +zHe7{AWJOjXnFRBLt9LmaYA0D73J2J5Cfx#Om;>=G9)3yt9Wr+UI_x8G0vzIC^zgVn+#-b?wh1om6Pha +zN|9?t8AhYc834)WX<1vsW(={I3ZrE&L9K}3ai=gCE^fm}ICu(%iSMX(bj{xzYyQvW +z7ERaria>lvZZmA^zIEVyC$P{kuf;41w{KqvqskeB=f3NO-G +z=Y{?QDq?4!w>japPeCADML4I#Azc&%BXI?_(P)IE9R*1-3s95c1QAW_)gz%9l3^g; +z+1yz;JY&$7w1SXYM~GTIz2ANN?U&ZU_xk!bbQrdvf7FggX>s?XtvL;)G6?EwvHT?w +zL@Ti%4)IQ~SY!`<83a=FbqdN~@Rl-fRl?@NQ3`@DOASv`I5I8Rh(;-p)*bYLY5Afb +zL2$+(hSTu12~_%`?>Yj8N88*czV@Ui%=@xP$WSVkN(OO^DK*6x-hKCirt8hV=3)bY +zXh0F=*kobi2V2iJRq+O#y58(ln6JI7F#W5l|=3rQG`;szm-KN*X +zMi=l#8_}cKzMyiKwp%R0XFzabz&t?UH2Mf4OL7#SITV};EzkG{f+5c`RPt^cjWlye +zdO_6M_vQQBj#|6+lm)=o4FCd8@|m}y{8wAgR-OYzYEq@qA_lD}@(~F`n~}Ox0)gb- +zNz1|JIS`Z&NUHv8Ckboos*^!PorW7x1O|I&(EJhS{CR&xAns`TBur+G!IAU8a~8Ql +z#AIArp5c8uXxVTZ2Q)J?Lnutlii(VkLmp{)f7=DkVB&cIF>KkOL8s>EeR94c^HEOa +z#k2b>e<6=du&~iAfn=a->JLB2cD4fUzD^}5MF5;y_?jVb!d2xh+bn`$3Y@umgA(Nu +z{g&o@dFcfJ;&ID`F|G(gO+_O`5D76Gscwa`m!n}3P>G1THak}nhx#?qw2}ctBZ62* +zGtZCfHMs@=$=;yvd2(5O_-Cc(8cJnF5QxxZ^rTK?$d}ar&CBv6`7Cz&<)B3pDwSB1 +z2gWE8XR9V<5O5eroeMRN(AHuKo!#R1=jB;~`9=>%$dV`o4=sTp5|Xe*T6w4_(eKdX +ziZKG#fq*U!qJgLg_(+Thaw(xIpcEB}$K{73bl8Tu;M$d{CL1@tr +z)F?)F+b(P$T27X(*1$Ho}O**y6jnARwT(>QeI=2@&#}! +ze%W}kDJ+h&N<|X +z9JXCpG6P7|wnS};M3><$9HcL_n(Kk!1T}osIeO`o8i8Ppqs~jfs#@}Z0mR5l&&%^$ +zbbS8l6Df=W3=TltX5R}S+>(#54Xtu+QwaT6PgA(M1~kWsdKsh(K>pQ!tVKVpu7Z|K +zJ|~j$A8bGk9rmU(-#i0yO~^N|JOz15Gci-clDJB8k{6>ea(I%O9-C@DlKH0kBg%E@ +z{sLjQ>2v^+*9{DQe|NXvrV~!*H`Oozs8f-|Pnz7=^C0e}xGcWa22Nt{v;>8a5C={I +z5iNNQ#DGqUeN@x0Zic!GP4YWB7L|>zX7!%eH|@@R`!9cT$O98Bi8p^QtVkde@-3__ +zD$<&M+*-pDDtvtqasL{{j#i*ULCePPPkA9Jk_ +zZ{nTd&Jy +zz$GS}dFS{V$b?`thh>mh`4*mt)o#3d=hm%-g&K^-*DZtLRRV&|5p!#R3O4{G2!tbTA&+q7-u&3TX_mrug2zTcflEe5+&B)eFr=fz;uG@E)^Ah>cz{#OuUL +z&7C_*yB-hM|Nf4Regz1Mp-dZe&guXxIsERVMz3(43xkje!M3{7?aaYd-<(q15%u{E +zhN}(J*i#Vr1f55q8CP#_uToh&-%2r1=%6yq742651d{oKV=emv38!wS#Un+m`~H~9 +zcNL0fEyyf8P~I8^DcTDlJo@$`LM&3GC~0W=Zfa5M_*_4Qu`PCzj(-vc5{w;$Pi+i_~e>XLm>Jp^Gvda(j-Q6jTOt3$%gv~}lD~hou +z$*x-~8AYkn?Z+-Vx&PC3_AzPXX&fj0*iPsn=CCUbN^RE~45lC4< +zx?WEY-*PNuoSg9VfIqC!9|ID+M+@fE1oH +z%llOpHD6koB?(brA{^dTc?)zfJd1e34jc>JTr5KqXXv5I8MO#U=n4oCdg7__b9zFE_7IFx*+;pWeL}ghn*NVdi^}$__JndHqoQ|~4FqT8g_##i +zxc}U6;j^Ssn~j<$2*hHDY6vw+?0J@&Jfbnj#UtZ>r}I=-onX0T1~$+C1A?9JFZ@FO +zM94=Vf)vQmN+psK0P+2>%Yme!hZgFqA@}E{QhXd0C8A)2FH_AS5W1qRSC^kbzd`vk +zh7uLW=<(#tx-yYacAnT(Ub*mSp`e=Xn`JHD@O*Q9=Go?)!-r31ox6-ckaO`!(&*i^ +zX+S>A2V0jBUdLCuZaSULn<0_l@qlVS=X$?@SG3E&kk6+;IExe)FDE$=2FITG4?yzM +z%MvPF#VHc1GARXC=?+s^2+f~V(1~?OrK)v(U^M=oh-xk~m2anl^veDFCsH1ov7xFxCHV +zphA$$%P+|93Q?w)vPkjA%MZE{hzO^~_CXGz3Uy7$=dwo%jYUujD!gf;s3n>M8GlM= +zWaz8uaa5b31M*MMrSpGYo9p~j)6{D7>$N5yuCZ8Rqc5f}{V3152vKtQX#JTPjd~A+ +zBw4p5YK*JRW@2AVAXYlsAw-=1e^Mfz5J0zX}XFsv`7r~h!i4*2T^@2XvxFT=msKr)2Gwx)2fkg@sZe~!~vqk +zvvs4+7phf-zH3w|D67roJ(AmU|LoW~N$W)k>|E!jpBPh(696GJUbovCBY(9BypZnr +zohO|Bn?J)6vEddw#zVgs`){3xD@XlRo{5^j2d?$-_{^v91wg2~eh%bNTJO}_D!r5% +z^FSat?yyD!tO~E1<3wbk4LqwzuUw^4Wfvb6nlh2e>s?V$lnfJo_q|H-k=I};&(#|L +za{uz*H53><(*L`kAcc(x#cR_6VHbp4BN4{Eh0k~Pth)s_y7dk +z0~t+vZrvKXjzGTIvxu^9p`y3io>Eqf$A}bIBoNrltCfu-&#I_c0?#ViP)?z?7C%)u +z2t<^}W~FckB%>M>$|du{Dgo*)!gf`Y +z9Id>CVg#!AS0_@zUN$$eHrEGohZ?{t0XR;%ArOeEWP&N0+q2r}d;p<$D9#`EEEm>RrEhUc-H2E(S6huM2U)H%<}FUn9prS=W2;$t!_94$x9 +zSBz1qu(92>UtAHRSsI5LBfG6Wr+KWuz9jQ#*7TAl&Vj({&4hK!9o?3G0>J?o1UVU- +zc{%8TwFg~6bXW%l*dPbxiafT);7o*Nt>Fd_Bf!wm*nDYoNGMbBaYv=;-d96I97rX2 +z+U-4%ip1K}>M0=_Kt)jtG#BoL>#7&WyNq}zr +zi1-RDxjXSoFszlLg2}AzXsnlCnLks1`e>HCEthMS@Cb+tBDU>zN3Gyw@Ev@7YwUSt +zas@bZprf1vL5C|CowcIVE{iMZ4YJ`O6IAbYXXO2Db&$);L^D&zxylMa7(wkGNP0nU +zVybgp$cVt-YcWsE76=$@R)$aggvPY!DcG}$^z;N(#d`}Ryw$)UFieCi1T*)SV%F_c +zgM>6|240jPlIf!#N}3Kp2r)_KZn-zDnn^?3IquMLOTFvJ2MqY_SdzkDF1DQJ!Zn+KgP+%B+o(q;~ds=5OWAgZ+{Zb6$_A +zQBRR41`@M4du4Monmb8~39(pANO!(D!Wm-^CVb=ZK9`GayUIqa;8#N)h?N?XX~=p_ +z=qvrh;G@;msYgA_`y9pByRo1BCw6rJa;UlwK$g(dxQq!Kz#8yrg;F=fZCTYzy`39H +zx`o>P3>$ue)kM4|D1=af!U(Ej?DM-HsN-hP>UbP@j9*P>WtBGmt5s6+eFutmH3tIT +zl^b=tw>4r?{0Z13cYKTuL}HN?NiZ0`u>~M-^{o);$+jAFo0c0}ME7E4D35KtqWBM6 +zXCKq#ode=WthJ)up?KlJl_ +z-(Q{|wY9Z%`?+`S9*A*(+ae$+kzK0L<4K4S5aS5P1<vIsxY?RnCFJoNC<({mZ$yO#392tq`n50`A5ZZw5c +zfrOvB_xbQet>a?+YH*m&o0^*Hbyg1OD?K_0wK@cO3H%i@4QEN@+@Xgx45X|K`rmC3 +z8zjG9#Z*i9cy_XUm^ngQ%@tth0CKvSp)`=fV?1=u$mZJ%MXwY4o#3f-i;g2KjPQV> +zX=>1#e`-}i4oG?<@D`4YWw8(LKHSsuLuu3)7V{NJ0R#aWrN41uF*^;7MC?8O0PUH0 +zDTm3ZGo8MSkMfxFUzNVE%Pjhw~HnOh$bi9+k;*k>=h!c9~O9F4i52|sZt5W8;Cuwd>UQSuuQ +zgirLVWaP=gtk1IUk9Gg|O;E{?O9bp9jgQLZmx8@-WG{Dqj35qlN?>r3pqs{fgHtsY +zQ`0J^cWxcd5U3i6Mn(F846?_7cs&t>p +zOlD^s?)mQU&~ju+K|qiLx~>>PsiM$ +znIJb;_Kby7NwOEhvR-t^8OR;LH)vSd+EWeY68Ep$Af#y}4xY@+%sQFz=v?U+9sRvx +z{Wb`gxzyCSaN3yPm%lDr?rqTRr{njVcAc-Hx7P>c^k|g+{RE!kMz2C~1-f^lsL127 +zVaK%SM8_`xqDslz1?iyZt+B1b0Cuj`8cbFmnHp{a$h&o?OeIJW${sVk*z^TluB*+% +zVR$&@#SB=hHJilazbezhdc9)1&~UY6C9{w1zS)tnyQR8y@3utXr4LB`ji(otf$TZa +ztlh!cPt+-irg&~IB=L!5hjH&8tp+ePt5w*j=V`!Q)z=sQy%>@d-!He1Lz13XcvN=p +zg&m>?@mB^rF4RGU|MVSztPRWpNV`xr%LR~_FFdl+B;hhvFhPmrgyWRf{EiN54&Mlzdy=*iF*>|^^@BjRn4)U-e#O=)i2*f6?6vSqtN12>4Kar6}VrWwSN +z=;62@(0|b9l5?Yj4Aw)GuINCe(E}VdUH8k|$1N6>P*y%-G!ruijy4bWGFWsigPH}p +z)mcymAfmZ#kR)cf*X=u73$C5Vp-qrNM1uabf&Ldz`Xwq2wbb_{IWu|Bo1t5~_w5^t +zgcGV22!NeumuxgHj#^|h_97ljgH}h;=};eTPXXa$Ra7GRaF>9TB~{CH60C>hP#)Sb +zakS|^|JTbrk*Giv3N|&wIVu|dgjqA``k-d5z1u7=6y!pJ;zJNf-{ssZmPw1OJ>+D# +za`~U3nDVqZ^ZN##6OdTP3P8l(C!b~REA77dkLoYk{n`2*V+Rm^V8GmXW8&4lqoa0u +zzlO{`bKLH9&W=?x&R5KbAY>dzah;?E!eVqqcke_;A&%(IefgiObIYNbUULO;y1?Pj +zp;#ns<}{0mW^K-v3jriI4g$K&%!ta{=jUaznJR9&zoMdKs@f;rP>y)>`LSX=4Mm~| +zo=MKkdec*S>!0jB(f2?|FwqD6rimLXqazUy&86Y~OPbx6nn9mpGg08U(h9EW_Y$EX +zR@RQxp#rirl|^^lL22C7AwgH9DuTx{50*ZEnsM{~p_9IW1c)9p +z6*c^bdsh}4Czj0hIq6nwbMTV8H%TsM2ncXYO@EEW?uQ^aOryB3om`3|;ZZrBJA*@& +zAQ1_d6e&;)LhJyR1rxB3Cd}Oxe=s8t^2l4&*RKZ^scPl@-DlRFZgDEaI#4U(w<3qm +zMDzeMJ^fSbN`7kG*taE;HV_gB=Pkmc1(U`_D?JtT#xc&jUsYkCe=l@^JRVrz+g2f4 +z0OVO+aB@8GS^&b*I4}xUn0SyQkQe9wjh$a;Q&}3oV;RT|S$bSwn;p2_>ZM +zLr>D$88_xylGJ6x<{|E+j0}~?-X$y}(~Z&~B9fR)8#jYmrBsMB4K(yNYFFBg%S>SP +z25oH9sji{d4rRv9!oYx~Py2oMrd8`s$FC24@WK1*cfP;pJLj0GP%3rw%$fMXLt=6E +zsl4edfiz;5u3_^I|60&HeCgQ5JCMd|jAuW1_{nfq#DYh&d}}o +z*T-(u^Kh{INS{FBy4_G5g&|2ObazZ$X<%wNEf +zhk)Zyjw|1Oj}YFt2@*7Xa^BnCrIT+q)FHo_x>~k_=Pu7vQg!fBjfHlq?TqWjWk +zzKZw#<(AWMoHK(01*`5Q(v0ufi27P8H}@uy=KPmL#zZ-{a#MfBR< +zmbnYN32VzHqZ_ddHh=0MLjHIL1-}ag%v^!+ +zu3HNWc_YNbhi6xhKYW%KT4WHS?X?YmD-u?WMPnOfA1G=I1j`W(UwC5pd=o_XVSoSP +z59f2KfZGZhFso>%6Hoa_9W!en?4gv-9YGXa1cPDsU +zKqLRGgLDP~MAv~2r*t%Qd~bNVn`MbOoJf=%kTspXvpSibTCHH+gd`fRI9$+b00hFE +zPtd#uLJ$O0O{zEo`7j!uHN6RVOhSpluVW!d~TUr-hNy*V +z13OkkD3y#*FjitWVN{dAyktpe0B*oFv +z1lo=uQf=G2zY$Mk;nb!l9l$0FckzSyS-z-28+k5+G_-@UVQ&@2;xSv +z(n4&BrKM=Wk)IlJK%t>Ul{jV!yk6T{xmYu|LK)~?@2}Ctrm+JKooSyhYO4DQ~ol@>F$VI?!YMNfwb1$*B1(@ +z=cX?=+Vw1g*zRqCeDT2F1(uHECihIBEKD{E>(P}K-oBbhBr`;)EDVXTv^0bjc8M%3 +zC?h1cF0Jg!wm|UTt7CXUjK8)C^7)k!;D80TI6moHPUZS&GX-h*KxB}k_G0gB2N1Ok +z((3__O+lomxpaCD_Lr1r0V?pyn1e{6f~m5n{<{o=w4 +zKWL-@7ZzcQ_(jB6K@eL$8BLZTp5#~aHUg=ZW0z3m!CGc-g6oxfz5pWUiipo+e)5{M +z)%^Z7w0GQafB(viuLnqAb@?K`pF6np)=9C`y>7EbsjxWD45mFeAE(3_uj$`6T3ZFk +zwe{kdSFS%g4A(ELJ!wotfO3wR+?FsHQ9wKJHi*ngogm^HtY$U{WGa8Fl1xIDF6tfA% +z0+fm1n?05Fm&STm4nZ_>MWx}{>S#N&T|OK^5=k#hnN&Cua0gOxC;bi_3J92i?(PT& +z@rR4N3_#ketRKue{Ly&|RRS6rm7j!Ysu`~1>s-37_7WJh^p}NSe4+eT$P}AP(O4|+ +zV(pp9?7C<-v07GZ_kpDg+Byf?h}4Wn5J%p-9Ye?XA6>y4)=)mX>kEe?slmXzao~|; +z4%Sx0}PyRL-3nN()jYC4=xp7RY;&2I;Z8Eja?x*7T?!U+RXR_kYZU +z!#zFW9Q`wA6yE|-g$AJ*N&A+&yE(@IX@OQDKC_TcvJPc_Kj`Zc4OO48rwD-Gw*(Xv_UIGaL4*?7e3`t5-R2lSh^xqd84Cs4}W^FDQn9zijsF13M{zVR~<`0dB +z;hww3Rk_uLO*yyZ^N(arMN#SjFcHEi60E_fZug`IjtJ^LVtno=lKj+Jze{_WszRIN1X*HUTCH>C_wc;+D)6YYT +z*RWmTUi`Puu_Uwkj6-qwu_Ue*kO&$%=o%J?6*rej_Ock3znkGIb6 +zWm&yS2Z9LS7slFgUx+?ilDgQBdj7`ruw|IVzJ@wV{&tD)G@SPTMW@9Wl5lcsuU~6` +z7raw|%Or|@Pnlh`7!!rA1H$`p;zz}+92Tp2bFmKDAL`nrC>)<{qBHso +zvJ6|o^vMxL?frh4XZ`3WdH7s_NI0p@{EElbnX*!yp;Vtx&K&w$&to`sW +z79>enm;xWhu;ZKKIN}-h!eBKZM6j$9~*Q(SlE*i_bHS0o#tPY +z5-j+ww|x>h9%`RLUixM!e%f0qVAe5GH83X6?!#^_j-M@lO@*-aD%NMF2;Hg^Wgh@}elrPA3o_&(- +zeNyws4es~%;K1o+pfG(Z!G-nFWzl7)ejRNxY?M~uI=I&MYuz@4>GLH*ptjlQJ`LYr +z*KIIVzBhKHIDwe`X2hc@gsdjzXxX%b<_#kc$vIHFi2)-XM1=fs(`g?0)M{lcJXwp< +zBgIdDXM&n-=+_%;1a?sE$oeN{r%w=8tFfAlQopAk +z%wrVN=r>)oZ0w7^M~Xi~qp6lEaABgF(ck7V3Un;@cg|ODuD7@fw~OZ;^TQV +z$&4AiUj}-4;o`6JV$Y4C2G +z8hVweUdzl78hWzD|&J_)oRr2JdJP +zA&lca);^P(q@hQb9-kqNXVo9An7Q3NoAtyRQw-@JUDD$oluryjE +z3{zzbZhStP-K;xw@Yxf-B=4h(p=4f`k8p2DH$>qQLPR!szD!2|vJ}J`C6=EoRwG^+ +z;`ZDv1SGVO+?IqSxpxSM^_V~@2E+~dZQdl+oz;TP1MX+XXwugMy?Z5AoZ7#R33Y@T +zM)w4;9L0szO3>6i#4fV3q49@wu&`zcvQ!d8!m*dpn&7pp0Y=;QbiyOzhC7)Ki7tDt +zXaIqysWqx53ZgHlO)|YRDG**$7&F{0a8VEECY`3;yx)F>2;4Xr&gC;Iqiqx;orWkF +z8xk0Ty-mK&z`^~Fbs#S;;Qd@1ZFJh4R`+H>Wx$xgn>^oka;w9~QfR>rS7lYHG?D#o +z6Jo`Qg_-DP +zX@kdURs~L5?afF*73QF!=HQ?vIysP;FNCMBfA*}*&%$eDHh5L|y~D=C^v8(wdtcYZ +z)8Q|56BuZ~3~KpF-oKg|5Uf@Ac15Z>sP<9hpm(E>^cgr8dMxGhn7mnWA+JPK+EGR; +zCfK+V1&Xi1M6CUFIA+oJqr(aF3W_=ph7h;IVlqq&xJ=d(CqczQwL>f*A$gJW_|iZw +z&>!^cGyI)UH(_%jFMta0ci8K;?^D#C4_`@%@wP6R4qvs8y@ecdj|*ia7Exg3*BpG4 +z%Dqav(-_hWolzv04-3Ygs)Z~U$`R?hQq2Is2`RWS%z4?!GF2CryzMjCEFg_Y%K+yz +zG8tm;0X{;XG5?BBT|pMZ296(fGUtoF_$Ryrso&s;Cc!g3a;pYOn-tjPvW+1)iAQ)I +zaPyG(wl0MZUqz_Z!4+oEh$t>QIaiZ+J1|fQdfugliOCAg+6D!~3<-k#gA8N#Rk3@5 +z&u3Yevetsi3m`sm2Ntt>FV(PfME~wR=LFu+2@Noy&wr###hgP3mjy&H03re#97OQ% +zsZ;NtktNoC?s@G44Num-@G1zw*?jMf)dA`SWJHyI-Lp=m +zyv8V97L8$~?>Sf(&Ee27TQvEf=-_%~EL56_n`*ZRVS`=4Ka4&HGjr9P8e3rf;8BK& +z&0s~H!Z|V-mPt9vUj?5&%Sa@;XK~`TS$ylgW4|1h&I!<9c6_zoDdR2)FLErHw%Sow +zwc_2ZKizcAMchMvZ^6OY8)uiUt&RwA(`3@dzgihQ1MSrNi;ruq-C+?oVa@U0x +z(>^4ei3Bedg+!LX52G(u@W4P&3sdv45%OawU(*aQat~OuEf?Hi6Zi>__qCd)nw0_j +zvUwA_6WQ5tnFsl_AZNz8L8L*=L4?0A>inj9l&C`AC71u=H +z?bu{Q_=al@1+|F&El|te2eQB@?#+g(D(LjFx>w=0X;CJ|CQc@tuin_)Rd$KH$Y9P9 +z${MAq+Ns2`>_SLAfKm9~%?U2bK6>hiDEbdUD#NMd$hR*wFx8TxWVY3Za +zM&tRPhR$htT-*KlZT-SGBy4YD;6aZfAz^Jt1`=ABifztn#D_;u)2WTa-Bo^EKL;=o +zDc6Ov2x3ybU1B6gkFjv-UvyFl^(EFkIb4ht2Z(*io4 +zW(6^Rp7OMxVh73mYH?bkbxgXB=+TL>U^8OY>=P$oXPkGAmF?6#80T +z+e?24uzuJC8?nCu`7)ef&Nu8x+`0%wOB9wmZ^(+|&$!T80~3uj?NRH)aNhf~#vN9e +zem1VW#bKd$SZ4ufS0-pzoJ%P7UWdT@8yg`1+kpYLV153t;UJy~P8@7sO+#{ePIXcSgw}v2XayA<>Jxh}D)tMOGRgJY0QEJs` +z{>aB;ssVeqKi-6L#(PnBpPuOu<4Rf*GWVk8BdMCd} +zc^_!LU3n2YWBEk1?0<%f@MkB;t#h0%&cixNCZn@Lft$eDVl6z=l@Ga}k<7cF5n!!o +zXet^Q3;AyG!j)+$=3U>7D5cEf)=YMZ)jSZ?)!6EoSa3kU!3W2Xn`K`PqR|ML`Ju!A)|K2`l1>ErJG>o*qIC72B&jHYe36od@P! +zi)qQ9Y7g*>N;Y4;sSLlPxvM;q-Tzw2m;Zx=x>{mk0;Ed5zA?Hb1FrDGc6-;m+iSFU +zc22aC&R^-iyw5vE$D?GWWo7A5o@@>d3_uD92sGM_-tlsdQ?ZbAnF4LsSxDj&0TFgO +zFbB*@;0<;Y0es>tB&~M12_up)gRS(Ce{seFR$9$~MC8~S%gCTV+2AIiH`gndEW2~H +z`z|RK5KuxIccy|!;Bkm8puw0EcWFE{ij71G*o4( +z0~y!3%z_nq1kdh3x<;XVQS{_v?Q3|H1so1Z#CL|Zm2Z&7-mTO?&1?U-oogOAE4Cm{ +z`d4o(XCnWH-J^hx&?7X^xHns&B`u2*skUy`s~w=0252bVaZy(}U?e5?u>fG!UbYaS +z4Gz$YBX|~|U$??YUR+zxw2g5F_OJB7viI^}qx|ouEswnc0o{D4T~~|912EVr9)4P& +zS=*@uBmgy>GC)sz_8A$Iga2y-R#LKP$zyVe7P=4Vrn@Q)Fp6mG;Nall=^07<{OPT~ +zPDD~5M}Py>^H&ikOMCrXaXjFMyNuyNg$gXaPOE4z3=$o3Jt(guFuvAQbA?*MR;Dx}r~+zsgJ +zzCtQ*$r?UAKNl$E39K|(pdcV17*;zU{VtG7{)QDicnC&XAit07AxkJs2xbNxkEh-l +ztI=-hZ#0{5e0{huHk5pMKFXUdk-_HT=8j~#**>ze%L-Vq--ELbc7OqlEqqgfDL$7| +z^zia3^m~7il#>&4bK{s6W!C%o9eQ_nw_LRXoq&)qk2e`~Carh!_+@C+^?4E@nB?8v +zrP(B~aF_-3_5wx4#3EgX2f|T2iDX6dBot9e+}zxz-+7y;fop?^#LWumnJ%(ER<|F> +z44(0)x_-m7iZI17bV#w5<;|{V>IZ-R+z|XI2d!L0M$z{_~PzI|b} +z_>I9TkwT-USfkDEyuoB7YJe7^SUeW*JCd>d31w)Viag>w +zE)Hcnu_U(A@CEh^w;UM0IVsDf+yNUB)lCpiM=a>2dMSVx95URpuHBLGh>h8fgM&77%eeba~6*@>lA8=;7iEw2QP4d^IvP +z8fpiWc?lq5kxp*C)nS|HY^i2ov(x?A!{1u(mk%xyJ_nmAsx{Zt=LV=Ta0-O}2|y4O +z5yIAhMw5|xp3lvw|Ps$0W*KZd^Wlj=W@{AaG=^es3_){Y~Jis`IYYiWN~ho|DLil1qRD5 +zN6xAlvXG=U-8`VKVHr!k-;5Bi)EfnJRTtvY$;jR$#e%~lxMV?xboY;JA{IT_^y}D0 +zw1mJ8tVoSO-(}absB6M8b$Zqe)Ok0$OkaA#I +z48@e8TAlv;PmB6dbP|{7<%qt@Ea>I;PRL4)=M`_G!A40Y$Xy1Mum)I0#!3<77H4)u +zI6c{)TUsy&o^*@2H9Bp>QJA#S8$`zN?+@z^IIQL|VxYEQfVw~Oc}Wq!FS`G2T=aDu +z-DMYe(1$x=331oN(i#yV%?Q)lcY`}FpGRp*74@@$fX%pE+dAGOh5QRhJ&mcaXOhk4 +zLi_pirw^Zws;d9n^#IE8T1ypZDX|crNABquU?iL2;Ql%4Vg5cNBt}OJdbLKnEi|`g2q%v70%eM&7 +z5gdFefu8Ix3n54MC +zW40SGT11ajrrm5AI24T?-2$|VMsU%VX}AMmt>Pr~B}#An{>%QG>_1FQYV^)CExzx2 +z&7E_9c!fpiCLci|F3H*eM2DQQRtQp4>V2RP=KX3ZVw#OXuFxj$VDmM&HQD{*dc7301976VQyI69%EFvxxn>qC&Lo-`%ImvM +zCv>AXKPcD26Z_;m`1pw)uF6Mp=RnShU^yM81!?jbl!v#-kSa#RLhSOG0?yp1YB6Jr +zW=GrO|0zIRSHiH?DYiO+$EpdMkwz#4I6V(J12-W0+dAo4J*?nDQrFI<*}a92Y%1bU +z`RC_4tyg7>R(8{ +zA8*g?PWv##WoF+p0bJe>whg#+(1_+A+)9HS$|n?k;(r=Le*vR;57rn)2& +zEkD8KBSZm#3Drt?t!*#s#>0+yUNysIKRg=t`KSOcSHieiUP0z8F_$tZ(ciPnq_o~@ +z%-{zhbs{i7 +zt~8q8%WO|MF(FE_ye*bl_-@NcA!S9$IMb6x0`e_oNF!hy5a)H^H)5)t(}ek4a1Nc~FF4@f;5aO%aB&3O%B8NuMWWCzYb`d> +zQ-&3)G|5M|pzcLy>pA(p=?3&XKn+v0^`HNsS?M0eb+60BxF|&Y{?>MI^x``)Vp}1V +z;<0N$BUc(0=p=y>zD3k_I~ +zMC>T|rn!T!wN%lqT@ +z&Afsj|04$m&CH2M?F|6yeqb+e`&JWTP^~~z(;c>5;z6RuFKe)%3j|YzeZB9c)5E08 +zvX9?L9%?PT7Vu(RAIXR}s*=I*@Qp<*vA{&7B2uwdBH$_I`33U5di9weG|3 +zx-Iy`1L`R>G-q<+w-{f5qc<7ls}^cT4Y^Qi+meHXFIDgqkt0wpdBZGY?LB+q9&o`T +zd18L5%R+44Ml^UNbEw58BXP#{+I#J1$;VGO`#6Grd<=RWgP+T+ktE6H^>C;%(}szj +zK;wt^oW!yG4Fz=zm4zKw@$Wdo`VJm=879kp$F&$uMP_qiKSB4L@SV)g55F9Rb=3ocrK>iqIRR9n!X0Do*Ldi{9M&^sg&T_TZz~>`tbXc$p%%BI% +z#MahUA?U0t#2ZA4_41*w&52#TXU^_G4)$#uGOnpIb{Gs?Bge_xP|beH;cUSBec^gk +zu;a`And#3j5LZ)LALL9lQ0{$A?tzx&K6M(;#M))7n&`7KTkT>KvjI7O4?mTa;X`81yn7WAir6 +z^Dv#2{~#3{X=5gyP*2v`3yoLJl)--n2rC2}*3n8(L~4ohHzT6QbyEu{!K3q#&p9Lp +z?3#RrZR0JWoh5V%Au%m2?uSB&RO!i99khjDd#7P;NaxJ<_f>mYXQOtXqBZifoWn1d5WC&hmG;&Gv(>!l)|)selJ-m-pz9Og@*rA +z%Xl~n+gHI_Rjy513U_dEaq-~ZLm%H7RpVbREoW=Zu*D?n%JFyy6(v}{RCOy +z>_wu--o5bv-4rRuWG0oN3a2+(f)C6nR0%>9HdI1mB`d{jE6Q4vSf>>{@~N-bGMc6~ +zn=1MB2?XIjZuOC!s@-pN5{60UUw-L4f1L-3Ohud?4)I$4Y&#w^A*ij(1$$3|Vskv} +z#YKCOBnHKh5QN8fd|k)wI{^HZj_1!`{L&>R(m@P^tYk*J)5>eCrio9{j>kWLDCGrM +z*O<)utCbjQiH>aHzD!~>SNyzV|B?uyizaR*!v`(g6N5ks=aSqWHk#wzbQOx2Ehc(>s +zfl`oSK+EzLOKDeK?n#pu;5qF1g-8bXyN##%K`x2R14CxOh8w&P-kz4U}>3Q=A& +zwAa>sCXe?|fR^Y+S9_jW;=!_GK`1Bc2HY6Y)*s}A##+#}239~LV&Q~wL&4n_6^@vW +z;nGUYJ$5-C#kJr2EtD&Ty$t-H)#GyT->}39LWB1gdo%LwqR8{YbRBL*-FCEc5iY{; +z#TpZ~y8yolNKuWi&enqz%<*)Y)j#ff)9q1ezkI|N7|zr3b=T|b>+m?)d% +zKJ;1@L~w8ZQn0MxZS*{ew-;Ohn^Jl!+U{m|QvgB~tai**t#d>0E=CMjN*SZ+36QnO +z4NrSN!Cd>9SLf?=!Hjh+ek}c}ND_U`vvi9(MS>7nGZ*lPm%4(7(bhfuTHod8y%;N{YO_KMV}N<7D)x5snD;XG +zzCOH#WK2$4mAvQWFCCZW#F8TRInJ+=$6eR`V~dES6+!6-=6lkVCHyCW^Bb-$@=b%3 +zi%hxQwAp^EOp|zR61~UikJsM89qE@P3@X5J>+K)hO6K`Z$80UqhLV&|mVt3wQ#G4H +zi4>T}s*jr9pkN+B@=LbuMW8^kzEFQde*yOdnXiUws9u#OD8dYzm?0F`qCm7pBCNNz +zOJB@PR!5?2&9Zw_Jg~i=TwmStKiYq1_@$ +zZKB*^u}y2o({7rV#Nl+8$2T5 +zthMF3X`+*;4Q-~&-*4NzrU=7>#}h=jB}<^tsAch7Ac~Vq;V7 +ziknpCHOP}_P8F&VE%6e`WG~EVa?$ra`knKZrYWbIZ_w@4vO+{B!(Pb&!YhY8pCfe= +zjxF8x>Zh3;#gw`fu})grVJcf=Ohg_Xc9m?(57$!NXQ#N%;Q{V}EjtmA$m<@Ie2(h2j9T2Xq=0<2R#daW&$ +z85=lCIqjn+?h$SF4u|?#DOOKg9>2c{9GSdlh{<(WR;Mb+bxH>u95roevUiqSmcdG* +zEL`{Qv+mA#hjLxuC*l?ROBgDsPYkDNU%;m09$2^ni=SVA=kS_) +z_h->URCbhQr89T-a-Gg9Dk?P`CT8-=f%@A28AYMmma&Ks#DNDsr^|eI%nHBQ0Nps* +z<{@u^G-9krSD|^{Vm?_nRkW_T!;E*n95To#4sxn;9FH2W%&T043S^Vg_Bk^^&J9*H +z=-^Zd6GYUG(CMkA?hy<&4Tc5fn4$3ys+ZiGw!07qHH1zPDzAJY;{8Oj#B1-LTAZ>D +zKqX)c%j0#o|H%z2zdkxYKaV6<&nEMgP`q%2&v+2dsa++rFeWoOnf$VkCAY6|8|kw{ +zdwe(maC?oeGlx#HVClH?)W&QZ`+=l3PIeQ%9cb~nWxJ9)YD|MPt`v?0-3bMcbZ<2Z +zG7xSnH{QoOr#C@?R{C$168|JMfCxcPAVuEhewgQpYO@AfbP3Fw+|Vi7h~L@$6ydj5 +zyf7_h9Rp$0Gii0mkT9xddqw>hIVCXV203~$D~swIj_)TV=zX)@-tK6Hb66mM;EywH +zsMV;{!i^8fvae3b)iz7_f6$4yU2i-b%Bh|o@eU2$RD^G(AtWlyl0^8dxd<9 +zCi_xU0%&wFugtmc%-uOk=xMY?lR%{7BQRZ~b8}1<=DQI)v2*#3|70VNVV*?SK4O}0 +z-HEICfCoyTwy@{F=Ac>4KISQEgQLDcj|>j}hzn(*RSn +zZw&u6!^Z2~7ae&u`+{IHYm_vxJJ@RRZ!LoCjQ2ecK6E;AqeyJZxfuAC +zaFBgBIQO4DawgA~vN)BCS%`;S38kn@9kWOTMq)$V$+z&4nDQvH*{(1#N58$C)v2#; +zJW|ch#FaXRBNNj6mX)HNV{_ScADWB7#Jn(Th}B15lvrI|-2fj-=SL1AY +zQrI&y#`tyxRIyenc$G7)m}|d;5&h;8q8?ap1~7v{vEXIAhojO|^XI$6=K!f+>;5yx +zJJXiq*Z?mW;Ak{?4<=)9$$a@6Q*=1_%}Nx&bGA3oqS%{I)k3y{#DALAzrPw)h(FU +zj}8a8Xte($dBpT +z_ZLeg50aO#zhmy?M*+dS#c4NyP>CZSyS+OOi>@2;)lr;&A$)(OEO;kV+bz6O57by +zyW>9>Ij2^Du|A83(r~$46%S7?Ancv(6R +zJK?TL+k$9p$KMJgY}hdrTzyS}0it==hvU?8YM**7M}l@-W{&s26~NM6 +z#U8(RCX-=6Lw%{$D&=aKSfE%aJ<__RASP1DaZcJPva<-yi3NH#t$OuNk6wlp&CD~1 +zanJ|7AhF;l{a^)Qhr_9Bo;2ZG8=}0whx#r7zZ6W`Fs5 +zJEbvhZVJVsORu$w4Y1HyT1E4?Vka&kS*mSpBuKM>OAT~3W;g7KLGzfQWF~QJ1)H6S +zFCOXwP_auqzKSygLBPB}EH;Q1gXb@Wm*lZWfM<8NWGZM_*$8Ze)0+^IpqCyco5T+P +z>!edzc-RMsx%H6~4%a*u{&6!V2Xf)f8oOKEEtBAhvI#TkSv+Ago-TMSQ(2q}=S0FP +zL(1v}1vp6Ya1@zfO!}Dq3ke|~@mmFXu2dHEQWpO$6X$;c8V@V*w>NACSkmSKF-THX +zXc85Wu2(uhx0b@}vaeA-YhO(oJ!8ZlugSxzOn{tnI7h@dCB`UVE~EEY_ww_|qDlb| +zQh0>qvDy{uar91x0J$!N&ch{3*B*?y730`NAZJT0IXU?T1Oo1Zc+QnB&!+ZYLh%_v +zV;)6DQs1sEzvoxu0r{lou-yG%CgwotYzFK>vqr!e>KRehvaz@y)fTge`_wgV2*|2H +zVl|vbxEx$3ymn~uGqN65%FYqJ<_)*Uqs49;KY2h*(Xa?Tk7AFfl-xf>irJoUyL*;0 +z19&1GQV*5Ni~#kTnaq0ymCiLjk_=0q&=&|cG{r57n*6NwV6zJl5K*ED&DsZy8iEL_rr +zgsLXr6cN9-S7dCo0TeKI3ByoGNNBIG{4b4m4=LB^FstU0B?!6TBZ1v~zn%e*Xk=B) +z@_rySE6iHcIxSfbe^sRAkjZKFfR!7A5uNa|Q%HSV{);)`X_I$=Rz#g9)RV +zjIuDE+A6IDHt@Noy^%sCnU|?kL3tCMU12QN7688MFeYr;%^{CT)BqX<4rY8gFNo(^2<+x6~@> +z0Y;8%xJK3sk3si!JoTyNPRqf>i>%mkw_b{g-~}-aAljQww_S1L53kdn=uMDZM5$#ndk +z&22o*u=b&^trc3UMGkzzrL*~$;t?gd{w8WCC+z$)6{fY`v4CL%;?|JZtR3}&oLz8* +zT?G#HsX)xAYvWho@h=pJpzsjcWp0%LD4s08onG)Nb4)MY=8K^XfVvcKVvP||0{idF +zr>Wx=dX&);ID@-|u5Y#BAa0c8rW_t)Xfo4c@By|jKCCPsr7DjJ6t;eTIrmF;CpM`~(ysWB=S@seY-cC;IYp7eGp3%$l} +z)oc?3jDrN<0qs>+yfj#>o^%eHp8`K^wUK{qUM_Xl#K;;VHK+>&$DqLQV1~BoxLuBrt&0}DAhEKn_^ER` +zz-29QNvC|8F%an87xNYKcn*LCu89T8nVkc&?~&O83)5GbY)slt*#=)i7s;A_C=2r7N7+fk`X1KngTDCyUEafq@X5m_z1=DeiD@Q38P{+Ou8AdwgrjC5 +zajlbj!7Ae^jZ~9GGnmvF%|dV*Siz7~1$lG}zFHP5%BV8TD09lQN!w79WRZ;`=PM(z +z0;YT`0PcRb5SM~SQ_OKjwTc~?W_G_IPe||U$;Um2U%fe+7X>%Nvy!xcXUbbT1miw0 +z=$X7_W&m0ay!h~`ae>C68mu@al*ia7R0saqO=sn$tE@ww372nWLhU^>%{WE>Eoln8 +zaeH(5Zly+xlW1Z@B{Z2HqS52V*oh`BC}k&quf19RS}N6$l#0qGWzl9DQkZ@85(#UMH4E) +z!&hPrOmR$HRF*}2C{e3A#U3h9d)gN68^|>O9=TO4Ga~u#5kl0}_*QP9IxEl~Ce;Vj +zS3zvyQ+p-TKYiV8z>J$akDBH=i$W7}&)8|aN%_17$7$H|;eKWRKgAtrMwoyE;#kJp +z>iJ{R+d4p$2q2;Y5EBQ7>@E&mk*MzVW>!EDsQ9Pd1Icl|=0d^U2HU!hP6MLe0bwp2 +zA=U!|OQM?{{^8dU?o^&w|I~Y5fw~zw)IT&*mzBRUy1Ljo^-=Z`fvN|N_JgxG~k*Hc%03VftQZkoi*AD{-11-bt2%}_=-R;7ZY`jOzsFyAEWb! +zVJNLPL#@4|8iv-c@m4Lu!^Uc7?VOsDWty>@T6^QN67|~9P?w&boWVpR2)d)gI@s*$ +zT0uPct)H#x^_Y(_q2El&g2<(pF8niAzCde(;c)XAp3awn@Z)3{qMO$l1?#O_cXL+a +zB+yS96Q;w{xIBw9%-h2xp$%a(D0`Noi$$31BbukCM_lu$4sG_+rWsH9U`eD0eY3t3 +z@`vkyB5OW$_NhyNPE(&_JPvYO1XVd%SiaJPVza|ZguGogD*p`OzJ!Odk4wR7o=G7; +zQFEN*_9WQcO`Vliy5G@VCnZ;Qb~fJ44e1$o^Tw=L_lA;Z-8Dw0CC}X_m5Q_J*xP61 +z2tVQGAnU9PA@k;{9QL{c=-~c_joC`W*8qxTI)7}foE-)SU;g6SD;S1P5oGCta0DrC +zGXz?khB$Fn{Ycwuk%t&RTyJ!Mz8mnC0U+AYu}PkaA-t-gE*25%;RVKNKyWz!scpu6 +zZDKFBX5S4#lCQK!Ip%UxMsP%cC4T!8d`;mo#M{(B)h;Ilk3UVA`-O^+JuQDuUnt-K +z=jEH2NuzvVs7mGT0rJ;Nz54;;pVk-{O`o<8h5~yAG9cx)%sJ+#d0-B8j!9{+{>1@9 +zYiz-m^g@6wE8^*umZD0JhIN!|&Ok-?2XhJ@B|oI&FfS^$rs90JhlZBoJW`e5b9j^- +zWO>uD9oB-o4QKEBn$akVeT1MeUX-s%#m~lPXZR!_h7SU~%Y_rx{QlrO`$o+{oUb!PIS+x5N +z+{O+YLa6?IE1#&A?RMZ&J}!O!vj>Os^y>J_BMi^Cu8;>FP)!5eagStg`4k8`f<9)s +zLv>uniXJHc5tD}2a*xO+UycHT8lGykAS#tq7H&?$Q|yXO#aH{77;M;}%#Rn*u_i#Q#=kFoCjB +zxM)O)sW@_wx=K{lJ|iyESH0iv9Nr111eP3eEA!SenTb%U12{RS*7qj0=;%^Kd#QiJ +ziYTEU=jFY{zWsSqmqmw<7L@5T1o7NxWhht`9gu$(b|QZnjVAE)D;lyC=>~hv=8piE3T9#-QVKCSaq-q&xr*zuRbfKtru+;Kkp5Si5+<6{tz}rp +zigZWmiiYYR#xdxCbhhJz=wN$k9zPcR8H;AJErv2><3*Bm51h&CEJlpT9yo5`1`w{pnaAJ%0k=ISmg0E +zo$J6^H1-w0!^WV5w|yx36dtal`WN}DGpD-gqYjDTfjIaLtR}xxCDSo6v=}KHRM^9@ +z&T;nw5x5ee(K3%Z3QQF%sMId_cIRpr&3g$f><9ZoX7X_c7g4f{y)mf(?;`TLI@jLv +z?N)ryzDJ)LsBZU+VnRH0X1E}KJ!}%#n_-hEY9w +z`8(=7Fd9^wGY;{_ggJK@ZR?yW!1!^^d;F^x%}=DG(7K8XMm$L~K*Np|t>vZmA5%Y| +zINrWxnZFq_J7&ksTGEluekfNRCX$8u^xk+?w8Q1iII^7LA8Wc=uh=>E34C14fN(+~ +zjb&LKSzG|ur8^cG=n*d|U)DK;5`-D7c>o{;1qb8{cYdL5^ll*Y29ag^ZWs(}{Dq?& +z7Vt6fu%BVSoqvD;RYW!I!KS^e-kCz_2@FvAByt<`2mpvxlE{aWp)% +z7->KZs4&!M+Z9|_;(QrbPRGNC2zLU&;bq*v@zaDlNR7 +zR!OB(0w7?XvMI3w1tc_A&fY$=RO&K>9q)K{?KeL9#X2nl`k!ouFF)XFC@Tui*%L4~ +zwNvTu3}=K5TH;uDS!^k3d+!l_hx$f?(hkYU(6NBYx@mz*Y6dZ7D@JF^5^p{aiT5zv +z;Xjc--#|sw407DGZz<4^FBXBq5F)zwTQ|65$~FTfyft2wOiY&QG(ydKoz#wa?YKny +z)9C@EX0c#XN}}K5dNFdMNo^+Os>0sS^c;E5Ky4zm)q;>J{J+z3sdUj)7tN@@gZSf7 +zJ|wiD$oI`e{Xe-gDV9P_(x}i7AaPVJn&m~NMi(84-RGbXy6@{lY?h66ze7!6Ee=i! +zInre-6PCHrI9+8v4+)Zge*esLVEy0*)t)o|)801Zf98hgQ=EZH2bpZ=)5NN_2yjw# +zP8Ewr(5WN{8DJpt*e!|G(gvZ5Pxywag$Agdns%%4+IH>|FMw9b +zKb<-v)*Cb*Ao~hb;B*`Ee&trZYBi`{$ru%gmKbuXcPNb3lD3H3Jimki7;BEFp{bxX +zFJ7Rk<~$d5(AGs1%w=$DDrj&3=?C4wX`U{m8^^=Z8R3YTB_A>ZAOkmldWl +zwo0ZyTNCB`dfUZA+chm*()HWtA2!JQ3>g${8%Vr% +zasf==&095e)fG}M%iIsk{PaQ>2|D59ppz^2pExvb9Ou9EI^`kN!0aXr*u3p0ex0b4 +z=AnHH#@v>`#o*LjN-yB0^^l)H2Nm=yD3|>1aNigv$f`s680kxF8B%d>SUG)YF0R~W +z$TI5rvll2~&q4RSwu3})*@1!~z4l}@NsY#MwV(2Y=hbLZh-ce*Eq3<#rZ +zxra}au9h@`-JaCDeW|)St?N40z`g~4rjZ?xu=?#W;cJyHNPXCV2DuxD%N1A2hAlFH +zwTJm(6XPn#dA&{dq>&yd{5Lp=pa<%$*em=~TdQ%rn_v#5`>I!IS>M^uNpl#N|wC@HMBcRTMT#SL;d7 +z<(&BuA6dLkkx|8fWw@PXzCeCBgDx@HJs@)L+j8y~gZ)7)${p-|O7{G? +z&|M6FI|A*^d_U+Of-3`+w(c~-YsQby|NH)g|G7xv|Nek^|Jex)g~z+)I0xPC0460S +LFIp>X81%mY^Bg|U + diff --git a/patches/server/0002-MC-Dev-fixes.patch b/patches/server/0002-MC-Dev-fixes.patch new file mode 100644 index 0000000..19f5319 --- /dev/null +++ b/patches/server/0002-MC-Dev-fixes.patch @@ -0,0 +1,49 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Fri, 31 Mar 2023 20:40:28 -0700 +Subject: [PATCH] MC-Dev fixes + + +diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java +index afbb027021acfbe25d534a84f1750e420bbde6e0..af6f91c66e9cc7e0d491e6efed992a140947155e 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/Brain.java ++++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java +@@ -79,7 +79,7 @@ public class Brain { + } + + public DataResult> decode(DynamicOps dynamicOps, MapLike mapLike) { +- MutableObject>>> mutableObject = new MutableObject<>( ++ MutableObject>>> mutableObject2 = new MutableObject<>( // Folia - decompile fix + DataResult.success(ImmutableList.builder()) + ); + mapLike.entries() +@@ -91,10 +91,10 @@ public class Brain { + DataResult> dataResult2 = dataResult.flatMap( + memoryType -> this.captureRead((MemoryModuleType)memoryType, dynamicOps, (T)pair.getSecond()) + ); +- mutableObject.setValue(mutableObject.getValue().apply2(Builder::add, dataResult2)); ++ mutableObject2.setValue(mutableObject2.getValue().apply2(Builder::add, dataResult2)); // Folia - decompile fix + } + ); +- ImmutableList> immutableList = mutableObject.getValue() ++ ImmutableList> immutableList = mutableObject2.getValue() // Folia - decompile fix + .resultOrPartial(Brain.LOGGER::error) + .map(Builder::build) + .orElseGet(ImmutableList::of); +@@ -194,14 +194,14 @@ public class Brain { + if (optional == null) { + throw new IllegalStateException("Unregistered memory fetched: " + type); + } else { +- return optional.map(ExpirableValue::getValue); ++ return (Optional)optional.map(ExpirableValue::getValue); // Folia - decompile fix + } + } + + @Nullable + public Optional getMemoryInternal(MemoryModuleType type) { + Optional> optional = this.memories.get(type); +- return optional == null ? null : optional.map(ExpirableValue::getValue); ++ return optional == null ? null : (Optional)optional.map(ExpirableValue::getValue); // Folia - decompile fix + } + + public long getTimeUntilExpiry(MemoryModuleType type) { diff --git a/patches/server/0003-Threaded-Regions.patch b/patches/server/0003-Threaded-Regions.patch new file mode 100644 index 0000000..48613c5 --- /dev/null +++ b/patches/server/0003-Threaded-Regions.patch @@ -0,0 +1,21096 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 2 Oct 2022 21:28:53 -0700 +Subject: [PATCH] Threaded Regions + +See https://docs.papermc.io/folia/reference/overview and +https://docs.papermc.io/folia/reference/region-logic + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +index 1d288e73fd8605676c0da676e068afb5b4b8abea..30eb7fd0b83ad1626d337cb770fac3dda5202344 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +@@ -78,17 +78,22 @@ public final class ChunkSystem { + } + + public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { +- ++ // Folia start - threaded regions ++ level.regioniser.addChunk(holder.getPos().x, holder.getPos().z); ++ // Folia end - threaded regions + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { + // Update progress listener for LevelLoadingScreen +- final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; ++ final ChunkProgressListener progressListener = null; // Folia - threaded regions - cannot schedule chunk task here; as it would create a chunkholder + if (progressListener != null) { + ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { + progressListener.onStatusChange(holder.getPos(), null); + }); + } ++ // Folia start - threaded regions ++ level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z); ++ // Folia end - threaded regions + } + + public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { +@@ -97,16 +102,12 @@ public final class ChunkSystem { + } + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + chunk.loadCallback(); + } + + public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + chunk.unloadCallback(); + } + +@@ -116,9 +117,7 @@ public final class ChunkSystem { + } + + public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { + chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); + } +@@ -128,22 +127,16 @@ public final class ChunkSystem { + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + } + + public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( +- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +index 217d1f908a36a5177ba3cbb80a33f73d4dab0fa0..301cc1c0d91f5e755f74ace60dbe5551240b496d 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +@@ -1,5 +1,11 @@ + package ca.spottedleaf.moonrise.common.util; + ++import io.papermc.paper.threadedregions.RegionShutdownThread; ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.RegionizedWorldData; ++import io.papermc.paper.threadedregions.ThreadedRegionizer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; + import net.minecraft.core.BlockPos; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.ChunkPos; +@@ -98,46 +104,149 @@ public class TickThread extends Thread { + } + + public static boolean isShutdownThread() { +- return false; ++ return Thread.currentThread().getClass() == RegionShutdownThread.class; + } + + public static boolean isTickThreadFor(final Level world, final BlockPos pos) { +- return isTickThread(); ++ return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4); + } + + public static boolean isTickThreadFor(final Level world, final ChunkPos pos) { +- return isTickThread(); ++ return isTickThreadFor(world, pos.x, pos.z); + } + + public static boolean isTickThreadFor(final Level world, final Vec3 pos) { +- return isTickThread(); ++ return isTickThreadFor(world, net.minecraft.util.Mth.floor(pos.x) >> 4, net.minecraft.util.Mth.floor(pos.z) >> 4); + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) { +- return isTickThread(); ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ return isShutdownThread(); ++ } ++ return ((net.minecraft.server.level.ServerLevel)world).regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region; + } + + public static boolean isTickThreadFor(final Level world, final AABB aabb) { +- return isTickThread(); ++ return isTickThreadFor( ++ world, ++ CoordinateUtils.getChunkCoordinate(aabb.minX), CoordinateUtils.getChunkCoordinate(aabb.minZ), ++ CoordinateUtils.getChunkCoordinate(aabb.maxX), CoordinateUtils.getChunkCoordinate(aabb.maxZ) ++ ); + } + + public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) { +- return isTickThread(); ++ return isTickThreadFor(world, CoordinateUtils.getChunkCoordinate(blockX), CoordinateUtils.getChunkCoordinate(blockZ)); + } + + public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { +- return isTickThread(); ++ final int fromChunkX = CoordinateUtils.getChunkX(position); ++ final int fromChunkZ = CoordinateUtils.getChunkZ(position); ++ ++ final int toChunkX = CoordinateUtils.getChunkCoordinate(position.x + deltaMovement.x); ++ final int toChunkZ = CoordinateUtils.getChunkCoordinate(position.z + deltaMovement.z); ++ ++ // expect from < to, but that may not be the case ++ return isTickThreadFor( ++ world, ++ Math.min(fromChunkX, toChunkX) - buffer, ++ Math.min(fromChunkZ, toChunkZ) - buffer, ++ Math.max(fromChunkX, toChunkX) + buffer, ++ Math.max(fromChunkZ, toChunkZ) + buffer ++ ); + } + + public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { +- return isTickThread(); ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ return isShutdownThread(); ++ } ++ ++ final int shift = ((net.minecraft.server.level.ServerLevel)world).regioniser.sectionChunkShift; ++ ++ final int minSectionX = fromChunkX >> shift; ++ final int maxSectionX = toChunkX >> shift; ++ final int minSectionZ = fromChunkZ >> shift; ++ final int maxSectionZ = toChunkZ >> shift; ++ ++ for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { ++ for (int secX = minSectionX; secX <= maxSectionX; ++secX) { ++ final int lowerLeftCX = secX << shift; ++ final int lowerLeftCZ = secZ << shift; ++ if (((net.minecraft.server.level.ServerLevel)world).regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { ++ return false; ++ } ++ } ++ } ++ ++ return true; + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) { +- return isTickThread(); ++ return isTickThreadFor(world, chunkX - radius, chunkZ - radius, chunkX + radius, chunkZ + radius); + } + + public static boolean isTickThreadFor(final Entity entity) { +- return isTickThread(); ++ if (entity == null) { ++ return true; ++ } ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ if (RegionizedServer.isGlobalTickThread()) { ++ if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { ++ final net.minecraft.server.network.ServerGamePacketListenerImpl possibleBad = serverPlayer.connection; ++ if (possibleBad == null) { ++ return true; ++ } ++ ++ final net.minecraft.network.PacketListener packetListener = possibleBad.connection.getPacketListener(); ++ if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { ++ return gamePacketListener.waitingForSwitchToConfig; ++ } ++ if (packetListener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { ++ return !configurationPacketListener.switchToMain; ++ } ++ return true; ++ } else { ++ return false; ++ } ++ } ++ if (isShutdownThread()) { ++ return true; ++ } ++ if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { ++ // off-main access to server player is never ok, server player is owned by one of global context or region context always ++ return false; ++ } ++ // only own entities that have not yet been added to the world ++ ++ // if the entity is removed, then it was in the world previously - which means that a region containing its location ++ // owns it ++ // if the entity has a callback, then it is contained in a world ++ return entity.hasNullCallback() && !entity.isRemoved(); ++ } ++ ++ final Level world = entity.level(); ++ if (world != region.regioniser.world) { ++ // world mismatch ++ return false; ++ } ++ ++ final RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ ++ // pass through the check if the entity is removed and we own its chunk ++ if (worldData.hasEntity(entity)) { ++ return true; ++ } ++ ++ if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { ++ net.minecraft.server.network.ServerGamePacketListenerImpl conn = serverPlayer.connection; ++ return conn != null && worldData.connections.contains(conn.connection); ++ } else { ++ return ((entity.hasNullCallback() || entity.isRemoved())) && isTickThreadFor((net.minecraft.server.level.ServerLevel)world, entity.chunkPosition()); ++ } + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +@@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter { + return slices == null || !slices.isPreventingStatusUpdates(); + } + ++ // Folia start - region threading ++ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world ++ public boolean addEntityForShutdownTeleportComplete(final Entity entity) { ++ final BlockPos pos = entity.blockPosition(); ++ final int sectionX = pos.getX() >> 4; ++ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); ++ final int sectionZ = pos.getZ() >> 4; ++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); ++ ++ return slices.addEntity(entity, sectionY); ++ } ++ // Folia end - region threading ++ + protected void removeEntity(final Entity entity) { + final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); + final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); +@@ -986,6 +999,9 @@ public abstract class EntityLookup implements LevelEntityGetter { + EntityLookup.this.removeEntityCallback(entity); + + this.entity.setLevelCallback(NoOpCallback.INSTANCE); ++ ++ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading ++ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading + } + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +index 58d9187adc188b693b6becc400f766e069bf1bf5..2b44515cede7dfa6d8dbfd469e53632b41dc1c21 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -18,7 +18,7 @@ public final class ServerEntityLookup extends EntityLookup { + private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; + + private final ServerLevel serverWorld; +- public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker ++ // Folia - move to regionized world data + + public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { + super(world, worldCallback); +@@ -76,6 +76,7 @@ public final class ServerEntityLookup extends EntityLookup { + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); + } ++ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading + } + + @Override +@@ -88,14 +89,14 @@ public final class ServerEntityLookup extends EntityLookup { + @Override + protected void entityStartLoaded(final Entity entity) { + // Moonrise start - entity tracker +- this.trackerEntities.add(entity); ++ this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading + // Moonrise end - entity tracker + } + + @Override + protected void entityEndLoaded(final Entity entity) { + // Moonrise start - entity tracker +- this.trackerEntities.remove(entity); ++ this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading + // Moonrise end - entity tracker + } + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -216,7 +216,7 @@ public final class RegionizedPlayerChunkLoader { + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + + if (loader == null) { +- return; ++ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading + } + + loader.remove(); +@@ -304,7 +304,7 @@ public final class RegionizedPlayerChunkLoader { + public void tick() { + TickThread.ensureTickThread("Cannot tick player chunk loader async"); + long currTime = System.nanoTime(); +- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { ++ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding + final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); + if (loader == null || loader.removed || loader.world != this.world) { + // not our problem anymore +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +@@ -29,6 +29,39 @@ public final class ChunkUnloadQueue { + + public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} + ++ // Folia start - threaded regions ++ public List retrieveForCurrentRegion() { ++ final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion region = ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion(); ++ final io.papermc.paper.threadedregions.ThreadedRegionizer regionizer = region.regioniser; ++ final int shift = this.coordinateShift; ++ ++ final List ret = new ArrayList<>(); ++ ++ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { ++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next(); ++ final long key = entry.getKey(); ++ final UnloadSection section = entry.getValue(); ++ final int sectionX = CoordinateUtils.getChunkX(key); ++ final int sectionZ = CoordinateUtils.getChunkZ(key); ++ final int chunkX = sectionX << shift; ++ final int chunkZ = sectionZ << shift; ++ ++ if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) { ++ continue; ++ } ++ ++ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); ++ } ++ ++ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { ++ return Long.compare(s1.order, s2.order); ++ }); ++ ++ return ret; ++ } ++ // Folia end - threaded regions ++ + public List retrieveForAllRegions() { + final List ret = new ArrayList<>(); + +@@ -141,4 +174,4 @@ public final class ChunkUnloadQueue { + this.order = order; + } + } +-} +\ No newline at end of file ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index 3990834a41116682d6ae779a3bf24b0fd989d97d..dbb5b6ee36a54d6682b2a6d9389aee721b95d506 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -57,6 +57,14 @@ import java.util.concurrent.atomic.AtomicReference; + import java.util.concurrent.locks.LockSupport; + import java.util.function.Predicate; + ++// Folia start - region threading ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.ThreadedRegionizer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++// Folia end - region threading ++ + public final class ChunkHolderManager { + + private static final Logger LOGGER = LogUtils.getClassLogger(); +@@ -79,29 +87,83 @@ public final class ChunkHolderManager { + private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); + private final ServerLevel world; + private final ChunkTaskScheduler taskScheduler; +- private long currentTick; ++ // Folia start - region threading ++ public static final class HolderManagerRegionData { ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } + +- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); +- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { +- if (c1 == c2) { +- return 0; ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ ++ public void merge(final HolderManagerRegionData into, final long tickOffset) { ++ // Order doesn't really matter for the pending full update... ++ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); ++ ++ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating ++ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant ++ // addition to every entry will not affect compareTo). ++ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { ++ holder.lastAutoSave += tickOffset; ++ into.autoSaveQueue.add(holder); ++ } + } + +- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { ++ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.pendingFullLoadUpdate.add(fullLoadUpdate); ++ } // else: fullLoadUpdate is an unloaded chunk holder ++ } + +- if (saveTickCompare != 0) { +- return saveTickCompare; ++ for (final NewChunkHolder autoSave : this.autoSaveQueue) { ++ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.autoSaveQueue.add(autoSave); ++ } // else: autoSave is an unloaded chunk holder ++ } + } ++ } + +- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); +- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); + +- if (coord1 == coord2) { +- throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ if (region == null) { ++ return null; + } + +- return Long.compare(coord1, coord2); +- }); ++ if (this.world != null && this.world != region.getData().world) { ++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); ++ } ++ ++ return region.getData().getHolderManagerRegionData(); ++ } ++ // Folia end - region threading ++ + + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + this.world = world; +@@ -186,8 +248,13 @@ public final class ChunkHolderManager { + } + + public void close(final boolean save, final boolean halt) { ++ // Folia start - region threading ++ this.close(save, halt, true, true, true); ++ } ++ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) { ++ // Folia end - region threading + TickThread.ensureTickThread("Closing world off-main"); +- if (halt) { ++ if (first && halt) { // Folia - region threading + LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); + if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); +@@ -197,9 +264,10 @@ public final class ChunkHolderManager { + } + + if (save) { +- this.saveAllChunks(true, true, true); ++ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading + } + ++ if (last) { // Folia - region threading + MoonriseRegionFileIO.flush(this.world); + + if (halt) { +@@ -221,28 +289,35 @@ public final class ChunkHolderManager { + } + + this.taskScheduler.setShutdown(true); ++ } // Folia - region threading + } + + void ensureInAutosave(final NewChunkHolder holder) { +- if (!this.autoSaveQueue.contains(holder)) { +- holder.lastAutoSave = this.currentTick; +- this.autoSaveQueue.add(holder); ++ // Folia start - region threading ++ final HolderManagerRegionData regionData = this.getCurrentRegionData(); ++ if (!regionData.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = RegionizedServer.getCurrentTick(); ++ regionData.autoSaveQueue.add(holder); ++ // Folia end - region threading + } + } + + public void autoSave() { + final List reschedule = new ArrayList<>(); +- final long currentTick = this.currentTick; ++ final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading + final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world)); + final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); +- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { +- final NewChunkHolder holder = this.autoSaveQueue.first(); ++ // Folia start - region threading ++ final HolderManagerRegionData regionData = this.getCurrentRegionData(); ++ for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) { ++ final NewChunkHolder holder = regionData.autoSaveQueue.first(); ++ // Folia end - region threading + + if (holder.lastAutoSave > maxSaveTime) { + break; + } + +- this.autoSaveQueue.remove(holder); ++ regionData.autoSaveQueue.remove(holder); // Folia - region threading + + holder.lastAutoSave = currentTick; + if (holder.save(false) != null) { +@@ -256,15 +331,38 @@ public final class ChunkHolderManager { + + for (final NewChunkHolder holder : reschedule) { + if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { +- this.autoSaveQueue.add(holder); ++ regionData.autoSaveQueue.add(holder); // Folia start - region threading + } + } + } + + public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { +- final List holders = this.getChunkHolders(); ++ // Folia start - region threading ++ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true); ++ } ++ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) { ++ final List holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10); ++ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone ++ // will multiply. to avoid this, we can simply iterate through all owned sections ++ final int regionShift = this.world.moonrise$getRegionChunkShift(); ++ final int width = 1 << regionShift; ++ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) { ++ final long sectionKey = iterator.nextLong(); ++ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift; ++ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; ++ ++ for (int dz = 0; dz < width; ++dz) { ++ for (int dx = 0; dx < width; ++dx) { ++ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz); ++ if (holder != null) { ++ holders.add(holder); ++ } ++ } ++ } ++ } ++ // Folia end - region threading + +- if (logProgress) { ++ if (first && logProgress) { // Folia - region threading + LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + +@@ -293,6 +391,12 @@ public final class ChunkHolderManager { + } + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); ++ // Folia start - region threading ++ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { ++ // skip holders that would fail the thread check ++ continue; ++ } ++ // Folia end - region threading + try { + final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); + if (saveStat != null) { +@@ -328,7 +432,7 @@ public final class ChunkHolderManager { + } + } + } +- if (flush) { ++ if (last && flush) { // Folia - region threading + MoonriseRegionFileIO.flush(this.world); + try { + MoonriseRegionFileIO.flushRegionStorages(this.world); +@@ -733,7 +837,13 @@ public final class ChunkHolderManager { + } + + public void tick() { +- ++this.currentTick; ++ // Folia start - region threading ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ throw new IllegalStateException("Not running tick() while on a region"); ++ } ++ // Folia end - region threading + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + +@@ -747,7 +857,7 @@ public final class ChunkHolderManager { + return removeDelay <= 0L; + }; + +- for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { ++ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) { + final long sectionKey = iterator.nextLong(); + + if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { +@@ -1032,26 +1142,56 @@ public final class ChunkHolderManager { + if (changedFullStatus.isEmpty()) { + return; + } +- if (!TickThread.isTickThread()) { +- this.taskScheduler.scheduleChunkTask(() -> { +- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); +- } + +- ChunkHolderManager.this.processPendingFullUpdate(); +- }, Priority.HIGHEST); +- } else { +- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ // Folia start - region threading ++ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>(); ++ final List thisRegionHolders = new ArrayList<>(); ++ ++ final int regionShift = this.world.moonrise$getRegionChunkShift(); ++ final ThreadedRegionizer.ThreadedRegion thisRegion ++ = TickRegionScheduler.getCurrentRegion(); ++ ++ for (final NewChunkHolder holder : changedFullStatus) { ++ final int regionX = holder.chunkX >> regionShift; ++ final int regionZ = holder.chunkZ >> regionShift; ++ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ); ++ ++ // region may be null ++ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) { ++ thisRegionHolders.add(holder); ++ } else { ++ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(holder); ++ } ++ } ++ if (!thisRegionHolders.isEmpty()) { ++ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders); ++ } ++ ++ if (!sectionToUpdates.isEmpty()) { ++ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ ++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift; ++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; ++ ++ final List regionHolders = entry.getValue(); ++ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { ++ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); ++ ChunkHolderManager.this.processPendingFullUpdate(); ++ }, Priority.HIGHEST); ++ + } + } ++ // Folia end - region threading + } + + private void removeChunkHolder(final NewChunkHolder holder) { + holder.onUnload(); +- this.autoSaveQueue.remove(holder); ++ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading + ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); + this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); + } +@@ -1064,7 +1204,7 @@ public final class ChunkHolderManager { + throw new IllegalStateException("Cannot unload chunks recursively"); + } + final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift +- final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); ++ final List unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions + int unloadCountTentative = 0; + for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { + final ChunkUnloadQueue.UnloadSection section +@@ -1382,7 +1522,13 @@ public final class ChunkHolderManager { + + // only call on tick thread + private boolean processPendingFullUpdate() { +- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ // Folia start - region threading ++ final HolderManagerRegionData data = this.getCurrentRegionData(); ++ if (data == null) { ++ return false; ++ } ++ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate; ++ // Folia end - region threading + + boolean ret = false; + +@@ -1393,9 +1539,7 @@ public final class ChunkHolderManager { + ret |= holder.handleFullStatusChange(changedFullStatus); + + if (!changedFullStatus.isEmpty()) { +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); +- } ++ this.addChangedStatuses(changedFullStatus); // Folia - region threading + changedFullStatus.clear(); + } + } +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -122,7 +122,7 @@ public final class ChunkTaskScheduler { + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; + public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; + +- private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); ++ // Folia - regionised ticking + + public final ChunkHolderManager chunkHolderManager; + +@@ -337,14 +337,13 @@ public final class ChunkTaskScheduler { + }; + + // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions +- this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); ++ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, Priority.BLOCKING); // Folia - region threading + // so, make the main thread pick it up + ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); + } + + public boolean executeMainThreadTask() { +- TickThread.ensureTickThread("Cannot execute main thread task off-main"); +- return this.mainThreadExecutor.executeTask(); ++ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking + } + + public void raisePriority(final int x, final int z, final Priority priority) { +@@ -829,7 +828,7 @@ public final class ChunkTaskScheduler { + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { +- return this.mainThreadExecutor.queueTask(run, priority); ++ throw new UnsupportedOperationException(); // Folia - regionised ticking + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +@@ -838,7 +837,7 @@ public final class ChunkTaskScheduler { + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final Priority priority) { +- return this.mainThreadExecutor.createTask(run, priority); ++ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +@@ -847,9 +846,27 @@ public final class ChunkTaskScheduler { + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final Priority priority) { +- return this.mainThreadExecutor.queueTask(run, priority); ++ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking + } + ++ // Folia start - region threading ++ // this function is guaranteed to never touch the ticket lock or schedule lock ++ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the ++ // ticket lock in the schedule logic ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, ++ final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); ++ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority); ++ }); ++ return ret; ++ } ++ // Folia end - region threading ++ + public boolean halt(final boolean sync, final long maxWaitNS) { + this.radiusAwareGenExecutor.halt(); + this.parallelGenExecutor.halt(); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +index eafa4e6d55cd0f9314ac0f2b96a7f48fbb5e1a4c..9e7e10fe46dbbd03d690a65af6ae719d1665bc6a 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -1360,10 +1360,10 @@ public final class NewChunkHolder { + private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { + // Update progress listener for LevelLoadingScreen + if (chunk != null) { +- final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; ++ final ChunkProgressListener progressListener = null; // Folia - threaded regions + if (progressListener != null) { + final ChunkStatus finalStatus = status; +- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions + progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); + }); + } +@@ -1384,7 +1384,7 @@ public final class NewChunkHolder { + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid +- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); +@@ -1412,7 +1412,7 @@ public final class NewChunkHolder { + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid +- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..c7ce32b31fc4247e72baa5f2dedac7378fa708c3 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -1940,7 +1940,7 @@ public final class CollisionUtil { + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { +- final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); ++ final ChunkAccess chunk = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, currChunkX, currChunkZ) ? null : chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); // Folia - region threading + + if (chunk == null) { + if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { +diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +index e7d510af3e415064fd483f0220d5f6a4cd0b9f63..8bc53743698df06cc31365e82c211257cfff6a82 100644 +--- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java ++++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +@@ -836,14 +836,14 @@ public class RedstoneWireTurbo { + j = getMaxCurrentStrength(upd, j); + int l = 0; + +- wire.shouldSignal = false; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading + // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, + // and I'm not ready to try to replicate even more functionality from + // elsewhere in Minecraft into this accelerator. So sadly, we must + // suffer the performance hit of this very expensive call. If there + // is consistency to what this call returns, we may be able to cache it. + final int k = worldIn.getBestNeighborSignal(upd.self); +- wire.shouldSignal = true; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading + + // The variable 'k' holds the maximum redstone power value of any adjacent blocks. + // If 'k' has the highest level of all neighbors, then the power level of this +diff --git a/src/main/java/io/papermc/paper/SparksFly.java b/src/main/java/io/papermc/paper/SparksFly.java +index 62e2d5704c348955bc8284dc2d54c933b7bcdd06..2ad5b9b0b7e18780ee73310451d9fa73f44c4bdb 100644 +--- a/src/main/java/io/papermc/paper/SparksFly.java ++++ b/src/main/java/io/papermc/paper/SparksFly.java +@@ -33,13 +33,13 @@ public final class SparksFly { + + private final Logger logger; + private final PaperSparkModule spark; +- private final ConcurrentLinkedQueue mainThreadTaskQueue; ++ // Folia - region threading + + private boolean enabled; + private boolean disabledInConfigurationWarningLogged; + + public SparksFly(final Server server) { +- this.mainThreadTaskQueue = new ConcurrentLinkedQueue<>(); ++ // Folia - region threading + this.logger = Logger.getLogger(ID); + this.logger.log(Level.INFO, "This server bundles the spark profiler. For more information please visit https://docs.papermc.io/paper/profiling"); + this.spark = PaperSparkModule.create(Compatibility.VERSION_1_0, server, this.logger, new PaperScheduler() { +@@ -50,7 +50,7 @@ public final class SparksFly { + + @Override + public void executeSync(final Runnable runnable) { +- SparksFly.this.mainThreadTaskQueue.offer(this.catching(runnable, "synchronous")); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(this.catching(runnable, "synchronous")); // Folia - region threading + } + + private Runnable catching(final Runnable runnable, final String type) { +@@ -88,10 +88,7 @@ public final class SparksFly { + } + + public void executeMainThreadTasks() { +- Runnable task; +- while ((task = this.mainThreadTaskQueue.poll()) != null) { +- task.run(); +- } ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + public void enableEarlyIfRequested() { +diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +index 14e412ebf75b0e06ab53a1c8f9dd1be6ad1e2680..3f733319482fedcf7461f4b7466e84afeae1fc2b 100644 +--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java ++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +@@ -83,7 +83,7 @@ public final class ChatProcessor { + final CraftPlayer player = this.player.getBukkitEntity(); + final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server)); + this.post(ae); +- if (listenersOnSyncEvent) { ++ if (false && listenersOnSyncEvent) { // Folia - region threading + final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients()); + se.setCancelled(ae.isCancelled()); // propagate cancelled state + this.queueIfAsyncOrRunImmediately(new Waitable() { +@@ -150,7 +150,7 @@ public final class ChatProcessor { + ae.setCancelled(cancelled); // propagate cancelled state + this.post(ae); + final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList()); +- if (listenersOnSyncEvent) { ++ if (false && listenersOnSyncEvent) { // Folia - region threading + this.queueIfAsyncOrRunImmediately(new Waitable() { + @Override + protected Void evaluate() { +diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +index 23432eea862c6df716d7726a32da3a0612a3fb77..f59e8bb72c5233f26a8a0d506ac64bb37fef97a5 100644 +--- a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java ++++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +@@ -23,35 +23,42 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider { + + public static final class CallbackManager { + +- private final Map callbacks = new HashMap<>(); +- private final Queue queue = new ConcurrentLinkedQueue<>(); ++ private final java.util.concurrent.ConcurrentHashMap callbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading ++ // Folia - region threading + + private CallbackManager() { + } + + public UUID addCallback(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { + final UUID id = UUID.randomUUID(); +- this.queue.add(new StoredCallback(callback, options, id)); ++ final StoredCallback scb = new StoredCallback(callback, options, id); // Folia - region threading ++ this.callbacks.put(scb.id(), scb); // Folia - region threading + return id; + } + + public void handleQueue(final int currentTick) { + // Evict expired entries + if (currentTick % 100 == 0) { +- this.callbacks.values().removeIf(callback -> !callback.valid()); ++ this.callbacks.values().removeIf(StoredCallback::expired); // Folia - region threading - don't read uses field + } + +- // Add entries from queue +- StoredCallback callback; +- while ((callback = this.queue.poll()) != null) { +- this.callbacks.put(callback.id(), callback); +- } ++ // Folia - region threading + } + + public void runCallback(final @NotNull Audience audience, final UUID id) { +- final StoredCallback callback = this.callbacks.get(id); +- if (callback != null && callback.valid()) { //TODO Message if expired/invalid? +- callback.takeUse(); ++ // Folia start - region threading ++ final StoredCallback[] use = new StoredCallback[1]; ++ this.callbacks.computeIfPresent(id, (final UUID keyInMap, final StoredCallback value) -> { ++ if (!value.valid()) { ++ return null; ++ } ++ use[0] = value; ++ value.takeUse(); ++ return value.valid() ? value : null; ++ }); ++ final StoredCallback callback = use[0]; ++ if (callback != null) { //TODO Message if expired/invalid? ++ // Folia end - region threading + callback.callback.accept(audience); + } + } +diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java +index 7b58b2d6297800c2dcdbf7539e5ab8e7703f39f1..a587d83b78af4efc484f939529acf70834f60d7e 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommands.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java +@@ -19,6 +19,7 @@ public final class PaperCommands { + COMMANDS.put("paper", new PaperCommand("paper")); + COMMANDS.put("callback", new CallbackCommand("callback")); + COMMANDS.put("mspt", new MSPTCommand("mspt")); ++ COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Folia - region threading + } + + public static void registerCommands(final MinecraftServer server) { +diff --git a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +index f671b74e4179fc29bc600b52e456ba9f78d8bbd6..fa3367c7f2b3d509886b152d892fe2168d83c6ae 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +@@ -129,7 +129,7 @@ public final class EntityCommand implements PaperSubcommand { + final int z = (e.getKey().z << 4) + 8; + final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)")) + .hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN))) +- .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z)); ++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (128) + " " + z)); // Folia - region threading - avoid sync load here + sender.sendMessage(message); + }); + } else { +diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +index cd2e4d792e972b8bf1e07b8961594a670ae949cf..3ab8dbf2768a4ef8fb53af6f5431f7f6afe6d168 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +@@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; + public final class HeapDumpCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + this.dumpHeap(sender); ++ }); // Folia - region threading + return true; + } + +diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..48a43341b17247355a531164019d5cc9c5555f26 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +@@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED; + public final class ReloadCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + this.doReload(sender); ++ }); // Folia - region threading + return true; + } + +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 088b8fe5d144807f4da1e85b2fa34dfd21286f8c..4ed27c10b432ceebf4447ab8007bc3a1be09a06e 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -354,4 +354,18 @@ public class GlobalConfiguration extends ConfigurationPart { + public boolean disableChorusPlantUpdates = false; + public boolean disableMushroomBlockUpdates = false; + } ++ ++ // Folia start - threaded regions ++ public ThreadedRegions threadedRegions; ++ public class ThreadedRegions extends ConfigurationPart { ++ ++ public int threads = -1; ++ public int gridExponent = 2; ++ ++ @PostProcess ++ public void postProcess() { ++ io.papermc.paper.threadedregions.TickRegions.init(this); ++ } ++ } ++ // Folia end - threaded regions + } +diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +index b1c917d65076a3805e5b78cb946753f0c101e214..ad3d9251e88ce9a35b0f46e9f0f54e62559bd9f9 100644 +--- a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +@@ -505,6 +505,14 @@ public class WorldConfiguration extends ConfigurationPart { + public Chunks chunks; + + public class Chunks extends ConfigurationPart { ++ ++ // Folia start - region threading - force prevent moving into unloaded chunks ++ @PostProcess ++ public void postProcess() { ++ this.preventMovingIntoUnloadedChunks = true; ++ } ++ // Folia end - region threading - force prevent moving into unloaded chunks ++ + public AutosavePeriod autoSaveInterval = AutosavePeriod.def(); + public int maxAutoSaveChunksPerTick = 24; + public int fixedChunkInhabitedTime = -1; +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +index 3e82ea07ca4194844c5528446e2c4a46ff4acee5..adfd4c16809f6ddd9cc73e4bd845d7aed4925068 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +@@ -256,12 +256,7 @@ class PaperPluginInstanceManager { + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + +- try { +- this.server.getScheduler().cancelTasks(plugin); +- } catch (Throwable ex) { +- this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " +- + pluginName + " (Is it up to date?)", ex, plugin); // Paper +- } ++ // Folia - region threading + + // Paper start - Folia schedulers + try { +diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +index c03608fec96b51e1867f43d8f42e5aefb1520e46..127d96280cad2d4e5db574a089d67ad68977b34e 100644 +--- a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java ++++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +@@ -50,6 +50,14 @@ public final class EntityScheduler { + this.entity = Validate.notNull(entity); + } + ++ // Folia start - region threading ++ public boolean isRetired() { ++ synchronized (this.stateLock) { ++ return this.tickCount == RETIRED_TICK_COUNT; ++ } ++ } ++ // Folia end - region threading ++ + /** + * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback + * on all currently scheduled tasks. +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..261b3019878c31a9e44e56b6611899de6c00ebee +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java +@@ -0,0 +1,226 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.event.inventory.InventoryCloseEvent; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.TimeUnit; ++ ++public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ ThreadedRegionizer.ThreadedRegion shuttingDown; ++ ++ public RegionShutdownThread(final String name) { ++ super(name); ++ this.setUncaughtExceptionHandler((thread, thr) -> { ++ LOGGER.error("Error shutting down server", thr); ++ }); ++ } ++ ++ static ThreadedRegionizer.ThreadedRegion getRegion() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ return shutdownThread.shuttingDown; ++ } ++ return null; ++ } ++ ++ ++ static RegionizedWorldData getWorldData() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ // no fast path for shutting down ++ if (shutdownThread.shuttingDown != null) { ++ return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); ++ } ++ } ++ return null; ++ } ++ ++ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves ++ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because ++ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. ++ // Thus, the only operation that the shutdown thread will perform ++ ++ private void saveLevelData(final ServerLevel world) { ++ try { ++ world.saveLevelData(true); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); ++ } ++ } ++ ++ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, ++ final ServerLevel world) { ++ try { ++ this.shuttingDown = region; ++ final List pendingTeleports = world.removeAllRegionTeleports(); ++ if (pendingTeleports.isEmpty()) { ++ return; ++ } ++ final ChunkPos center = region.getCenterChunk(); ++ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); ++ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) { ++ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to()); ++ ++ // first, add entities to entity chunk so that they will be saved ++ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) { ++ // assume that world and position are set to destination here ++ node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination ++ world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root); ++ } ++ ++ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if ++ // there are any player passengers, that the later player saving will save the tree ++ pendingTeleport.rootVehicle().restore(); ++ ++ // now we are finished ++ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to()); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to complete pending teleports", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, ++ final boolean last) { ++ ChunkPos center = null; ++ try { ++ this.shuttingDown = region; ++ center = region.getCenterChunk(); ++ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); ++ region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ private void haltChunkSystem(final ServerLevel world) { ++ try { ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr); ++ } ++ } ++ ++ private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { ++ ChunkPos center = null; ++ try { ++ this.shuttingDown = region; ++ center = region.getCenterChunk(); ++ ++ final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get(); ++ ++ for (final ServerPlayer player : worldData.getLocalPlayers()) { ++ try { ++ // close inventory ++ if (player.containerMenu != player.inventoryMenu) { ++ player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT); ++ } ++ ++ // drop carried item ++ if (!player.containerMenu.getCarried().isEmpty()) { ++ ItemStack carried = player.containerMenu.getCarried(); ++ player.containerMenu.setCarried(ItemStack.EMPTY); ++ player.drop(carried, false); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to close player inventory for player: " + player, thr); ++ } ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ @Override ++ public final void run() { ++ // await scheduler termination ++ LOGGER.info("Awaiting scheduler termination for 60s..."); ++ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.info("Scheduler halted"); ++ } else { ++ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); ++ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time"); ++ } ++ ++ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc ++ // halt all chunk systems first so that any in-progress chunk generation stops ++ LOGGER.info("Halting chunk systems..."); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ try { ++ world.moonrise$getChunkTaskScheduler().halt(false, 0L); ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable); ++ } ++ } ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ this.haltChunkSystem(world); ++ } ++ LOGGER.info("Halted chunk systems"); ++ ++ LOGGER.info("Finishing pending teleports..."); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ final List> ++ regions = new ArrayList<>(); ++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.finishTeleportations(regions.get(i), world); ++ } ++ } ++ LOGGER.info("Finished pending teleports"); ++ ++ LOGGER.info("Saving all worlds"); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'"); ++ ++ final List> ++ regions = new ArrayList<>(); ++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); ++ ++ LOGGER.info("Closing player inventories..."); ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.closePlayerInventories(regions.get(i)); ++ } ++ LOGGER.info("Closed player inventories"); ++ ++ LOGGER.info("Saving chunks..."); ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.saveRegionChunks(regions.get(i), (i + 1) == len); ++ } ++ LOGGER.info("Saved chunks"); ++ ++ LOGGER.info("Saving level data..."); ++ this.saveLevelData(world); ++ LOGGER.info("Saved level data"); ++ ++ LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'"); ++ } ++ LOGGER.info("Saved all worlds"); ++ ++ // Note: only save after world data and pending teleportations ++ LOGGER.info("Saving all player data..."); ++ MinecraftServer.getServer().getPlayerList().saveAll(); ++ LOGGER.info("Saved all player data"); ++ ++ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) ++ // done, part 2 should call exit() ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java +@@ -0,0 +1,235 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.Validate; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import javax.annotation.Nullable; ++import java.util.function.Supplier; ++ ++/** ++ * Use to manage data that needs to be regionised. ++ *

++ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd. ++ * The data is held in reference to the world it resides in. ++ *

++ *

++ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. ++ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced ++ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. ++ * See below for more details on instancing per world. ++ *

++ *

++ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current ++ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below ++ * see why the behavior may or may not be desirable: ++ *

++ *         {@code
++ *         public class EntityTickList {
++ *             private final List entities = new ArrayList<>();
++ *
++ *             public void addEntity(Entity e) {
++ *                 this.entities.add(e);
++ *             }
++ *
++ *             public void removeEntity(Entity e) {
++ *                 this.entities.remove(e);
++ *             }
++ *         }
++ *
++ *         public class World {
++ *
++ *             // callback is left out of this example
++ *             // note: world != null here
++ *             public final RegionizedData entityTickLists =
++ *                 new RegionizedData<>(this, () -> new EntityTickList(), ...);
++ *
++ *             public void addTickingEntity(Entity e) {
++ *                 // What we expect here is that this world is the
++ *                 // current ticking region's world.
++ *                 // If that is true, then calling this.entityTickLists.get()
++ *                 // will retrieve the current region's EntityTickList
++ *                 // for this world, which is fine since the current
++ *                 // region is contained within this world.
++ *
++ *                 // But if the current region's world is not this world,
++ *                 // and if the world check is disabled, then we will actually
++ *                 // retrieve _this_ world's EntityTickList for the region,
++ *                 // and NOT the EntityTickList for the region's world.
++ *                 // This is because the RegionizedData object is instantiated
++ *                 // per world.
++ *                 this.entityTickLists.get().addEntity(e);
++ *             }
++ *         }
++ *
++ *         public class TickTimes {
++ *
++ *             private final List tickTimesNS = new ArrayList<>();
++ *
++ *             public void completeTick(long timeNS) {
++ *                 this.tickTimesNS.add(timeNS);
++ *             }
++ *
++ *             public double getAverageTickLengthMS() {
++ *                 double sum = 0.0;
++ *                 for (long time : tickTimesNS) {
++ *                     sum += (double)time;
++ *                 }
++ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
++ *             }
++ *         }
++ *
++ *         public class Server {
++ *             public final List worlds = ...;
++ *
++ *             // callback is left out of this example
++ *             // note: world == null here, because this RegionizedData object
++ *             // is not instantiated per world, but rather globally.
++ *             public final RegionizedData tickTimes =
++ *                  new RegionizedData<>(null, () -> new TickTimes(), ...);
++ *         }
++ *         }
++ *     
++ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking ++ * is enabled for it by passing the world to the constructor. ++ *

++ */ ++public final class RegionizedData { ++ ++ private final ServerLevel world; ++ private final Supplier initialValueSupplier; ++ private final RegioniserCallback callback; ++ ++ /** ++ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must ++ * never produce {@code null} values. ++ *

++ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking ++ * operations may deadlock the entire server and as such the function should be completely non-blocking ++ * and must complete in a timely manner. ++ *

++ *

++ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever ++ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} ++ * for an entity tick list is invalid since the entities are tied to a world and region, ++ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to ++ * region only. ++ *

++ * @param world The world in which the region data resides. ++ * @param supplier Initial value supplier used to lazy initialise region data. ++ * @param callback Region callback to manage this regionised data. ++ */ ++ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { ++ this.world = world; ++ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); ++ this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); ++ } ++ ++ T createNewValue() { ++ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); ++ } ++ ++ RegioniserCallback getCallback() { ++ return this.callback; ++ } ++ ++ /** ++ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @return the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @throws IllegalStateException If the following are true: The server is in region ticking mode, ++ * this {@code RegionizedData}'s world is not {@code null}, ++ * and the current ticking region's world does not match this {@code RegionizedData}'s world. ++ */ ++ public @Nullable T get() { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ ++ if (region == null) { ++ return null; ++ } ++ ++ if (this.world != null && this.world != region.getData().world) { ++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); ++ } ++ ++ return region.getData().getOrCreateRegionizedData(this); ++ } ++ ++ /** ++ * Class responsible for handling merge / split requests from the regioniser. ++ *

++ * It is critical to note that each function is called while holding the region lock. ++ *

++ */ ++ public static interface RegioniserCallback { ++ ++ /** ++ * Completely merges the data in {@code from} to {@code into}. ++ *

++ * Calculating Tick Offsets: ++ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines ++ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in ++ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from ++ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for ++ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} ++ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, ++ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and ++ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate ++ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. ++ *

++ *

++ * Critical Notes: ++ *

  • ++ *
      ++ * This function is called while the region lock is held, so any blocking operations may ++ * deadlock the entire server and as such the function should be completely non-blocking and must complete ++ * in a timely manner. ++ *
    ++ *
      ++ * This function may not throw any exceptions, or the server will be left in an unrecoverable state. ++ *
    ++ *
  • ++ *

    ++ * ++ * @param from The data to merge from. ++ * @param into The data to merge into. ++ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. ++ */ ++ public void merge(final T from, final T into, final long fromTickOffset); ++ ++ /** ++ * Splits the data in {@code from} into {@code dataSet}. ++ *

    ++ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. ++ * To convert from chunk coordinates to region coordinates and keys, see the code below: ++ *

    ++         *         {@code
    ++         *         int chunkX = ...;
    ++         *         int chunkZ = ...;
    ++         *
    ++         *         int regionSectionX = chunkX >> chunkToRegionShift;
    ++         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    ++         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    ++         *         }
    ++         *     
    ++ *

    ++ *

    ++ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the ++ * data that is owned by the region which occupies the region section. ++ *

    ++ *

    ++ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because ++ * the new regions formed from the split will start at the same tick number, and so no adjustment is required. ++ *

    ++ * ++ * @param from The data to split from. ++ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. ++ * @param regionToData Lookup hash table from region section key to . ++ * @param dataSet The data set to split into. ++ */ ++ public void split( ++ final T from, final int chunkToRegionShift, ++ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fc053ded0c14b76a1c6c82b59d3fd320372a3293 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -0,0 +1,455 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; ++import net.minecraft.CrashReport; ++import net.minecraft.ReportedException; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketListener; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.chat.Component; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.dedicated.DedicatedServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.world.level.GameRules; ++import org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.List; ++import java.util.concurrent.CopyOnWriteArrayList; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BooleanSupplier; ++ ++public final class RegionizedServer { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final RegionizedServer INSTANCE = new RegionizedServer(); ++ ++ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue(); ++ ++ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); ++ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); ++ ++ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); ++ ++ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); ++ ++ public static RegionizedServer getInstance() { ++ return INSTANCE; ++ } ++ ++ public void addConnection(final Connection conn) { ++ this.connections.add(conn); ++ } ++ ++ public boolean removeConnection(final Connection conn) { ++ return this.connections.remove(conn); ++ } ++ ++ public void addWorld(final ServerLevel world) { ++ this.worlds.add(world); ++ } ++ ++ public void init() { ++ // call init event _before_ scheduling anything ++ new RegionizedServerInitEvent().callEvent(); ++ ++ // now we can schedule ++ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ TickRegions.getScheduler().scheduleRegion(this.tickHandle); ++ TickRegions.getScheduler().init(); ++ } ++ ++ public void invalidateStatus() { ++ this.lastServerStatus = 0L; ++ } ++ ++ public void addTaskWithoutNotify(final Runnable run) { ++ this.globalTickQueue.add(run); ++ } ++ ++ public void addTask(final Runnable run) { ++ this.addTaskWithoutNotify(run); ++ TickRegions.getScheduler().setHasTasks(this.tickHandle); ++ } ++ ++ /** ++ * Returns the current tick of the region ticking. ++ * @throws IllegalStateException If there is no current region. ++ */ ++ public static long getCurrentTick() throws IllegalStateException { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ if (TickThread.isShutdownThread()) { ++ return 0L; ++ } ++ throw new IllegalStateException("No currently ticking region"); ++ } ++ return region.getData().getCurrentTick(); ++ } ++ ++ public static boolean isGlobalTickThread() { ++ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); ++ } ++ ++ public static void ensureGlobalTickThread(final String reason) { ++ if (!isGlobalTickThread()) { ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { ++ return INSTANCE.tickHandle; ++ } ++ ++ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final RegionizedServer server; ++ ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ private final AtomicBoolean ticking = new AtomicBoolean(); ++ ++ public GlobalTickTickHandle(final RegionizedServer server) { ++ super(null, SchedulerThreadPool.DEADLINE_NOT_SET); ++ this.server = server; ++ } ++ ++ /** ++ * Only valid to call BEFORE scheduled!!!! ++ */ ++ final void setInitialStart(final long start) { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double scheduling global tick"); ++ } ++ this.updateScheduledStart(start); ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return !this.ticking.getAndSet(true); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.ticking.getAndSet(false); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ this.drainTasks(); ++ this.server.globalTick(tickCount); ++ } ++ ++ private void drainTasks() { ++ while (this.runOneTask()); ++ } ++ ++ private boolean runOneTask() { ++ final Runnable run = this.server.globalTickQueue.poll(); ++ if (run == null) { ++ return false; ++ } ++ ++ // TODO try catch? ++ run.run(); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ do { ++ if (!this.runOneTask()) { ++ return false; ++ } ++ } while (canContinue.getAsBoolean()); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return !this.server.globalTickQueue.isEmpty(); ++ } ++ } ++ ++ private long lastServerStatus; ++ private long tickCount; ++ ++ /* ++ private final java.util.Random random = new java.util.Random(4L); ++ private final List> walkers = ++ new java.util.ArrayList<>(); ++ static final int PLAYERS = 500; ++ static final int RAD_BLOCKS = 1000; ++ static final int RAD = RAD_BLOCKS >> 4; ++ static final int RAD_BIG_BLOCKS = 100_000; ++ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; ++ static final int VD = 4 + 12; ++ static final int BIG_PLAYERS = 250; ++ static final double WALK_CHANCE = 0.3; ++ static final double TP_CHANCE = 0.2; ++ static final double TASK_CHANCE = 0.2; ++ ++ private ServerLevel getWorld() { ++ return this.worlds.get(0); ++ } ++ ++ private void init2() { ++ for (int i = 0; i < PLAYERS; ++i) { ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = this.random.nextInt(-rad, rad + 1); ++ int posZ = this.random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ ServerLevel world = RegionizedServer.this.getWorld(); ++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); ++ }); ++ } ++ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) ++ ); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ServerLevel world = RegionizedServer.this.getWorld(); ++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); ++ }); ++ } ++ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) ++ ); ++ } ++ }; ++ ++ map.add(posX, posZ, VD); ++ ++ walkers.add(map); ++ } ++ } ++ ++ private void randomWalk() { ++ if (this.walkers.isEmpty()) { ++ this.init2(); ++ return; ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (this.random.nextDouble() > WALK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i); ++ ++ int updateX = this.random.nextInt(-1, 2); ++ int updateZ = this.random.nextInt(-1, 2); ++ ++ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() >= TP_CHANCE) { ++ continue; ++ } ++ ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); ++ ++ map.update(posX, posZ, VD); ++ } ++ } ++ */ ++ ++ private void globalTick(final int tickCount) { ++ /* ++ if (false) { ++ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null); ++ } ++ this.randomWalk(); ++ */ ++ ++this.tickCount; ++ // expire invalid click command callbacks ++ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount); ++ ++ // scheduler ++ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); ++ ++ // commands ++ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); ++ ++ // needs ++ // player ping sample ++ // world global tick ++ // connection tick ++ ++ // tick player ping sample ++ this.tickPlayerSample(); ++ ++ // tick worlds ++ for (final ServerLevel world : this.worlds) { ++ this.globalTick(world, tickCount); ++ } ++ ++ // tick connections ++ this.tickConnections(); ++ ++ // player list ++ MinecraftServer.getServer().getPlayerList().tick(); ++ } ++ ++ private void tickPlayerSample() { ++ final MinecraftServer mcServer = MinecraftServer.getServer(); ++ ++ final long currtime = System.nanoTime(); ++ ++ // player ping sample ++ // copied from MinecraftServer#tickServer ++ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing ++ // an uncomplete status ++ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { ++ this.lastServerStatus = currtime; ++ mcServer.rebuildServerStatus(); ++ } ++ } ++ ++ public static boolean isNotOwnedByGlobalRegion(final Connection conn) { ++ final PacketListener packetListener = conn.getPacketListener(); ++ ++ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) { ++ return !gamePacketListener.waitingForSwitchToConfig; ++ } ++ ++ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { ++ return configurationPacketListener.switchToMain; ++ } ++ ++ return false; ++ } ++ ++ private void tickConnections() { ++ final List connections = new ArrayList<>(this.connections); ++ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging ++ for (final Connection conn : connections) { ++ if (!conn.becomeActive()) { ++ continue; ++ } ++ ++ if (isNotOwnedByGlobalRegion(conn)) { ++ // we actually require that the owning regions remove the connection for us, as it is possible ++ // that ownership is transferred back to us ++ continue; ++ } ++ ++ if (!conn.isConnected()) { ++ this.removeConnection(conn); ++ conn.handleDisconnection(); ++ continue; ++ } ++ ++ try { ++ conn.tick(); ++ } catch (final Exception exception) { ++ if (conn.isMemoryConnection()) { ++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); ++ } ++ ++ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); ++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); ++ ++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { ++ conn.disconnect(ichatmutablecomponent); ++ })); ++ conn.setReadOnly(); ++ continue; ++ } ++ } ++ } ++ ++ // A global tick only updates things like weather / worldborder, basically anything in the world that is ++ // NOT tied to a specific region, but rather shared amongst all of them. ++ private void globalTick(final ServerLevel world, final int tickCount) { ++ // needs ++ // worldborder tick ++ // advancing the weather cycle ++ // sleep status thing ++ // updating sky brightness ++ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking ++ ++ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, ++ // this may not be the case - and thus, only the global tick thread can do anything. ++ world.taskQueueRegionData.drainGlobalChunkTasks(); ++ ++ // worldborder tick ++ this.tickWorldBorder(world); ++ ++ // weather cycle ++ this.advanceWeatherCycle(world); ++ ++ // sleep status ++ this.checkNightSkip(world); ++ ++ // update raids ++ this.updateRaids(world); ++ ++ // sky brightness ++ this.updateSkyBrightness(world); ++ ++ // time ticking (TODO API synchronisation?) ++ this.tickTime(world, tickCount); ++ ++ world.updateTickData(); ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates ++ } ++ ++ private void updateRaids(final ServerLevel world) { ++ world.getRaids().globalTick(); ++ } ++ ++ private void checkNightSkip(final ServerLevel world) { ++ world.tickSleep(); ++ } ++ ++ private void advanceWeatherCycle(final ServerLevel world) { ++ world.advanceWeatherCycle(); ++ } ++ ++ private void updateSkyBrightness(final ServerLevel world) { ++ world.updateSkyBrightness(); ++ } ++ ++ private void tickWorldBorder(final ServerLevel world) { ++ world.getWorldBorder().tick(); ++ } ++ ++ private void tickTime(final ServerLevel world, final int tickCount) { ++ if (world.tickTime) { ++ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ world.setDayTime(world.levelData.getDayTime() + (long)tickCount); ++ } ++ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); ++ } ++ } ++ ++ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a2313b5b4c37e8536973a8ea0b371557ea912473 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +@@ -0,0 +1,807 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.Iterator; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public final class RegionizedTaskQueue { ++ ++ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public static final class WorldRegionTaskData { ++ private final ServerLevel world; ++ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); ++ private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ public WorldRegionTaskData(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ private boolean executeGlobalChunkTask() { ++ final Runnable run = this.globalChunkTask.poll(); ++ if (run != null) { ++ run.run(); ++ return true; ++ } ++ return false; ++ } ++ ++ public void drainGlobalChunkTasks() { ++ while (this.executeGlobalChunkTask()); ++ } ++ ++ public void pushGlobalChunkTask(final Runnable run) { ++ this.globalChunkTask.add(run); ++ } ++ ++ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { ++ final ThreadedRegionizer regioniser = this.world.regioniser; ++ final ThreadedRegionizer.ThreadedRegion region ++ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); ++ if (region == null) { ++ return null; ++ } ++ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); ++ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); ++ } ++ ++ private void removeTicket(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void addTicket(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void processTicketUpdates(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord)); ++ } ++ ++ // note: only call on acquired referenceCountData ++ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) { ++ if (!referenceCountData.addedTicket) { ++ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc ++ this.addTicket(coord); ++ this.processTicketUpdates(coord); ++ referenceCountData.addedTicket = true; ++ } ++ } ++ ++ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) { ++ if (!referenceCountData.decreaseReferenceCount()) { ++ return; ++ } // else: need to remove ticket ++ ++ // note: it is possible that another thread increments and then removes the reference before we can, so ++ // use ifPresent ++ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { ++ if (valueInMap.referenceCount.get() != 0L) { ++ return valueInMap; ++ } ++ ++ // note: valueInMap may not be referenceCountData ++ ++ // possible to invoke this outside of the compute call, but not required and requires additional logic ++ WorldRegionTaskData.this.removeTicket(keyInMap); ++ ++ return null; ++ }); ++ } ++ ++ private ReferenceCountData incrementReference(final long coord) { ++ ReferenceCountData referenceCountData = this.referenceCounters.get(coord); ++ ++ if (referenceCountData != null && referenceCountData.addCount()) { ++ this.ensureTicketAdded(coord, referenceCountData); ++ return referenceCountData; ++ } ++ ++ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { ++ if (valueInMap == null) { ++ // sets reference count to 1 ++ return new ReferenceCountData(); ++ } ++ // OK if we add from 0, the remove call will use compute() and catch this race condition ++ valueInMap.referenceCount.getAndIncrement(); ++ ++ return valueInMap; ++ }); ++ ++ this.ensureTicketAdded(coord, referenceCountData); ++ ++ return referenceCountData; ++ } ++ } ++ ++ private static final class ReferenceCountData { ++ ++ public final AtomicLong referenceCount = new AtomicLong(1L); ++ public volatile boolean addedTicket; ++ ++ // returns false if reference count is 0, otherwise increments ref count ++ public boolean addCount() { ++ int failures = 0; ++ for (long curr = this.referenceCount.get();;) { ++ for (int i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ ++ if (curr == 0L) { ++ return false; ++ } ++ ++ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) { ++ return true; ++ } ++ ++ ++failures; ++ } ++ } ++ ++ // returns true if new reference count is 0 ++ public boolean decreaseReferenceCount() { ++ final long res = this.referenceCount.decrementAndGet(); ++ if (res >= 0L) { ++ return res == 0L; ++ } else { ++ throw new IllegalStateException("Negative reference count"); ++ } ++ } ++ } ++ ++ public static final class RegionTaskQueueData { ++ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); ++ private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); ++ private final WorldRegionTaskData worldRegionTaskData; ++ ++ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { ++ this.worldRegionTaskData = worldRegionTaskData; ++ } ++ ++ void mergeInto(final RegionTaskQueueData into) { ++ this.tickTaskQueue.mergeInto(into.tickTaskQueue); ++ this.chunkQueue.mergeInto(into.chunkQueue); ++ } ++ ++ public boolean executeTickTask() { ++ return this.tickTaskQueue.executeTask(); ++ } ++ ++ public boolean executeChunkTask() { ++ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); ++ } ++ ++ void split(final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ this.tickTaskQueue.split( ++ false, regioniser, into ++ ); ++ this.chunkQueue.split( ++ true, regioniser, into ++ ); ++ } ++ ++ public void drainTasks() { ++ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; ++ final PrioritisedQueue chunkTaskQueue = this.chunkQueue; ++ ++ int allowedTickTasks = tickTaskQueue.getScheduledTasks(); ++ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); ++ ++ boolean executeTickTasks = allowedTickTasks > 0; ++ boolean executeChunkTasks = allowedChunkTasks > 0; ++ boolean executeGlobalTasks = true; ++ ++ do { ++ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); ++ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); ++ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); ++ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); ++ ++ if (allowedChunkTasks > 0) { ++ // if we executed chunk tasks, we should try to process ticket updates for full status changes ++ this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); ++ } ++ } ++ ++ public boolean hasTasks() { ++ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); ++ } ++ } ++ ++ static final class PrioritisedQueue { ++ private final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { ++ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { ++ this.queues[i] = new ArrayDeque<>(); ++ } ++ } ++ private boolean isDestroyed; ++ ++ public int getScheduledTasks() { ++ synchronized (this) { ++ int ret = 0; ++ ++ for (final ArrayDeque queue : this.queues) { ++ ret += queue.size(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ public boolean isEmpty() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = Priority.IDLE.priority; ++ synchronized (this) { ++ for (int i = 0; i <= max; ++i) { ++ if (!queues[i].isEmpty()) { ++ return false; ++ } ++ } ++ return true; ++ } ++ } ++ ++ public void mergeInto(final PrioritisedQueue target) { ++ synchronized (this) { ++ this.isDestroyed = true; ++ mergeInto(target, this.queues); ++ } ++ } ++ ++ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { ++ synchronized (target) { ++ final ArrayDeque[] otherQueues = target.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ final ArrayDeque intoQ = otherQueues[i]; ++ ++ // it is possible for another thread to queue tasks into the target queue before we do ++ // since only the ticking region can poll, we don't have to worry about it when they are being queued - ++ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) ++ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the ++ // front of the queue, but we need to use descending iterator to ensure we do not reverse ++ // the order of elements from fromQ ++ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { ++ intoQ.addFirst(iterator.next()); ++ } ++ } ++ } ++ } ++ ++ // into is a map of section coordinate to region ++ public void split(final boolean isChunkData, ++ final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ final Reference2ReferenceOpenHashMap, ArrayDeque[]> ++ split = new Reference2ReferenceOpenHashMap<>(); ++ final int shift = regioniser.sectionChunkShift; ++ synchronized (this) { ++ this.isDestroyed = true; ++ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting ++ ++ // first, build the targets ++ final ArrayDeque[] thisQueues = this.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ ++ for (final ChunkBasedPriorityTask task : fromQ) { ++ final int sectionX = task.chunkX >> shift; ++ final int sectionZ = task.chunkZ >> shift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final ThreadedRegionizer.ThreadedRegion ++ region = into.get(sectionKey); ++ if (region == null) { ++ throw new IllegalStateException(); ++ } ++ ++ split.computeIfAbsent(region, (keyInMap) -> { ++ final ArrayDeque[] ret = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ ++ for (int k = 0; k < ret.length; ++k) { ++ ret[k] = new ArrayDeque<>(); ++ } ++ ++ return ret; ++ })[i].add(task); ++ } ++ } ++ ++ // merge the targets into their queues ++ for (final Iterator, ArrayDeque[]>> ++ iterator = split.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, ArrayDeque[]> ++ entry = iterator.next(); ++ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); ++ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); ++ } ++ } ++ } ++ ++ /** ++ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true ++ * if the task was added ++ */ ++ private Boolean tryPush(final ChunkBasedPriorityTask task) { ++ final ArrayDeque[] queues = this.queues; ++ synchronized (this) { ++ final Priority priority = task.getPriority(); ++ if (priority == Priority.COMPLETING) { ++ return null; ++ } ++ if (this.isDestroyed) { ++ return Boolean.FALSE; ++ } ++ queues[priority.priority].addLast(task); ++ return Boolean.TRUE; ++ } ++ } ++ ++ private boolean executeTask() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = Priority.IDLE.priority; ++ ChunkBasedPriorityTask task = null; ++ ReferenceCountData referenceCounter = null; ++ synchronized (this) { ++ if (this.isDestroyed) { ++ throw new IllegalStateException("Attempting to poll from dead queue"); ++ } ++ ++ search_loop: ++ for (int i = 0; i <= max; ++i) { ++ final ArrayDeque queue = queues[i]; ++ while ((task = queue.pollFirst()) != null) { ++ if ((referenceCounter = task.trySetCompleting(i)) != null) { ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ if (task == null) { ++ return false; ++ } ++ ++ try { ++ task.executeInternal(); ++ } finally { ++ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); ++ } ++ ++ return true; ++ } ++ ++ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { ++ ++ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData(); ++ static { ++ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE); ++ } ++ ++ private final WorldRegionTaskData world; ++ private final int chunkX; ++ private final int chunkZ; ++ private final long sectionLowerLeftCoord; // chunk coordinate ++ private final boolean isChunkTask; ++ ++ private volatile ReferenceCountData referenceCounter; ++ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class); ++ private Runnable run; ++ private volatile Priority priority; ++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class); ++ ++ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, ++ final Runnable run, final Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.isChunkTask = isChunkTask; ++ this.run = run; ++ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); ++ this.setPriorityPlain(priority); ++ ++ final int regionShift = world.world.regioniser.sectionChunkShift; ++ final int regionMask = (1 << regionShift) - 1; ++ ++ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); ++ } ++ ++ private Priority getPriorityVolatile() { ++ return (Priority)PRIORITY_HANDLE.getVolatile(this); ++ } ++ ++ private void setPriorityPlain(final Priority priority) { ++ PRIORITY_HANDLE.set(this, priority); ++ } ++ ++ private void setPriorityVolatile(final Priority priority) { ++ PRIORITY_HANDLE.setVolatile(this, priority); ++ } ++ ++ private Priority compareAndExchangePriority(final Priority expect, final Priority update) { ++ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void setReferenceCounterPlain(final ReferenceCountData value) { ++ REFERENCE_COUNTER_HANDLE.set(this, value); ++ } ++ ++ private ReferenceCountData getReferenceCounterVolatile() { ++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this); ++ } ++ ++ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { ++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void executeInternal() { ++ try { ++ this.run.run(); ++ } finally { ++ this.run = null; ++ } ++ } ++ ++ private void cancelInternal() { ++ this.run = null; ++ } ++ ++ private boolean tryComplete(final boolean cancel) { ++ int failures = 0; ++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ ++failures; ++ continue; ++ } ++ ++ // we have the reference count, we win no matter what. ++ this.setPriorityVolatile(Priority.COMPLETING); ++ ++ try { ++ if (cancel) { ++ this.cancelInternal(); ++ } else { ++ this.executeInternal(); ++ } ++ } finally { ++ if (curr != REFERENCE_COUNTER_NOT_SET) { ++ this.world.decrementReference(curr, this.sectionLowerLeftCoord); ++ } ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean isQueued() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean queue() { ++ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { ++ return false; ++ } ++ ++ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); ++ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { ++ // we don't expect race conditions here, so it is OK if we have to needlessly reference count ++ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); ++ return false; ++ } ++ ++ boolean synchronise = false; ++ for (;;) { ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ synchronise = true; ++ continue; ++ } ++ // may have been cancelled before we got to the queue ++ if (this.getReferenceCounterVolatile() != null) { ++ throw new IllegalStateException("Expected null ref count when queue does not exist"); ++ } ++ // the task never could be polled from the queue, so we return false ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return true; ++ } ++ } ++ ++ private ReferenceCountData trySetCompleting(final int minPriority) { ++ // first, try to set priority to EXECUTING ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr.isLowerPriority(minPriority)) { ++ return null; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) { ++ break; ++ } // else: continue ++ } ++ ++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ // something acquired before us ++ return null; ++ } ++ ++ if (curr == REFERENCE_COUNTER_NOT_SET) { ++ throw new IllegalStateException(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ continue; ++ } ++ ++ return curr; ++ } ++ } ++ ++ private void updatePriorityInQueue() { ++ boolean synchronise = false; ++ for (;;) { ++ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile(); ++ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { ++ // cancelled or not queued ++ return; ++ } ++ ++ if (this.getPriorityVolatile() == Priority.COMPLETING) { ++ // cancelled ++ return; ++ } ++ ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ synchronise = true; ++ continue; ++ } ++ // must have been removed ++ return; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ return; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return; ++ } ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.getPriorityVolatile(); ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public long getSubOrder() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ return this.setPriority(priority); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr == priority) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean execute() { ++ return this.tryComplete(false); ++ } ++ ++ @Override ++ public boolean cancel() { ++ return this.tryComplete(true); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1b741d4bccfd45beeec43300f44770516c0d850e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -0,0 +1,770 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.CrashReport; ++import net.minecraft.ReportedException; ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.chat.Component; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.util.VisibleForDebug; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.village.VillageSiege; ++import net.minecraft.world.entity.item.ItemEntity; ++import net.minecraft.world.level.BlockEventData; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Explosion; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.NaturalSpawner; ++import net.minecraft.world.level.ServerExplosion; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.RedStoneWireBlock; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.TickingBlockEntity; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.material.Fluid; ++import net.minecraft.world.level.pathfinder.PathTypeCache; ++import net.minecraft.world.level.redstone.CollectingNeighborUpdater; ++import net.minecraft.world.level.redstone.NeighborUpdater; ++import net.minecraft.world.ticks.LevelTicks; ++import org.bukkit.craftbukkit.block.CraftBlockState; ++import org.slf4j.Logger; ++import javax.annotation.Nullable; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++ ++public final class RegionizedWorldData { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; ++ ++ public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() { ++ @Override ++ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) { ++ // connections ++ for (final Connection conn : from.connections) { ++ into.connections.add(conn); ++ } ++ // time ++ final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime; ++ // entities ++ for (final ServerPlayer player : from.localPlayers) { ++ into.localPlayers.add(player); ++ into.nearbyPlayers.addPlayer(player); ++ } ++ for (final Entity entity : from.allEntities) { ++ into.allEntities.add(entity); ++ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ } ++ for (final Entity entity : from.loadedEntities) { ++ into.loadedEntities.add(entity); ++ } ++ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) { ++ into.entityTickList.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) { ++ into.navigatingMobs.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.trackerEntities.iterator(); iterator.hasNext();) { ++ into.trackerEntities.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.trackerUnloadedEntities.iterator(); iterator.hasNext();) { ++ into.trackerUnloadedEntities.add(iterator.next()); ++ } ++ // block ticking ++ into.blockEvents.addAll(from.blockEvents); ++ // ticklists use game time ++ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset); ++ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); ++ ++ // tile entity ticking ++ for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { ++ into.pendingBlockEntityTickers.add(tileEntityWrapped); ++ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ if (tileEntity != null) { ++ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ } ++ } ++ for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { ++ into.blockEntityTickers.add(tileEntityWrapped); ++ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ if (tileEntity != null) { ++ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ } ++ } ++ ++ // ticking chunks ++ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { ++ into.entityTickingChunks.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { ++ into.tickingChunks.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { ++ into.chunks.add(iterator.next()); ++ } ++ // redstone torches ++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { ++ if (into.redstoneUpdateInfos == null) { ++ into.redstoneUpdateInfos = new ArrayDeque<>(); ++ } ++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { ++ info.offsetTime(fromRedstoneTimeOffset); ++ into.redstoneUpdateInfos.add(info); ++ } ++ } ++ // mob spawning ++ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick); ++ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick); ++ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick); ++ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) { ++ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay); ++ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); ++ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); ++ } ++ // chunkHoldersToBroadcast ++ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { ++ into.chunkHoldersToBroadcast.add(chunkHolder); ++ } ++ } ++ ++ @Override ++ public void split(final RegionizedWorldData from, final int chunkToRegionShift, ++ final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ // connections ++ for (final Connection conn : from.connections) { ++ final ServerPlayer player = conn.getPlayer(); ++ final ChunkPos pos = player.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .connections.add(conn); ++ } ++ // entities ++ for (final ServerPlayer player : from.localPlayers) { ++ final ChunkPos pos = player.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); ++ into.localPlayers.add(player); ++ into.nearbyPlayers.addPlayer(player); ++ } ++ for (final Entity entity : from.allEntities) { ++ final ChunkPos pos = entity.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); ++ into.allEntities.add(entity); ++ // Note: entityTickList is a subset of allEntities ++ if (from.entityTickList.contains(entity)) { ++ into.entityTickList.add(entity); ++ } ++ // Note: loadedEntities is a subset of allEntities ++ if (from.loadedEntities.contains(entity)) { ++ into.loadedEntities.add(entity); ++ } ++ // Note: navigatingMobs is a subset of allEntities ++ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) { ++ into.navigatingMobs.add(mob); ++ } ++ if (from.trackerEntities.contains(entity)) { ++ into.trackerEntities.add(entity); ++ } ++ if (from.trackerUnloadedEntities.contains(entity)) { ++ into.trackerUnloadedEntities.add(entity); ++ } ++ } ++ // block ticking ++ for (final BlockEventData blockEventData : from.blockEvents) { ++ final BlockPos pos = blockEventData.pos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events ++ // is just some list. So if it unloads, I guess it's just lost. ++ if (into != null) { ++ into.blockEvents.add(blockEventData); ++ } ++ } ++ ++ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); ++ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); ++ ++ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ReferenceMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final RegionizedWorldData worldData = entry.getValue(); ++ ++ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks); ++ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks); ++ } ++ ++ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData); ++ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); ++ ++ // tile entity ticking ++ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { ++ final BlockPos pos = tileEntity.getPos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ if (into != null) { ++ into.pendingBlockEntityTickers.add(tileEntity); ++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets ++ // marked as removed. So if there is no section, it's probably removed! ++ } ++ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { ++ final BlockPos pos = tileEntity.getPos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ if (into != null) { ++ into.blockEntityTickers.add(tileEntity); ++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets ++ // marked as removed. So if there is no section, it's probably removed! ++ } ++ // time ++ for (final RegionizedWorldData regionizedWorldData : dataSet) { ++ regionizedWorldData.redstoneTime = from.redstoneTime; ++ } ++ // ticking chunks ++ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { ++ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); ++ final ChunkPos pos = holder.chunk().getPos(); ++ ++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .entityTickingChunks.add(holder); ++ } ++ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { ++ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); ++ final ChunkPos pos = holder.chunk().getPos(); ++ ++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .tickingChunks.add(holder); ++ } ++ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { ++ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); ++ final ChunkPos pos = holder.chunk().getPos(); ++ ++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .chunks.add(holder); ++ } ++ ++ // redstone torches ++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { ++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { ++ final BlockPos pos = info.pos; ++ ++ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift)); ++ if (worldData != null) { ++ if (worldData.redstoneUpdateInfos == null) { ++ worldData.redstoneUpdateInfos = new ArrayDeque<>(); ++ } ++ worldData.redstoneUpdateInfos.add(info); ++ } // else: chunk unloaded ++ } ++ } ++ // mob spawning ++ for (final RegionizedWorldData regionizedWorldData : dataSet) { ++ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; ++ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; ++ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; ++ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; ++ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; ++ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; ++ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid ++ } ++ // chunkHoldersToBroadcast ++ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { ++ final ChunkPos pos = chunkHolder.getPos(); ++ ++ // Possible for get() to return null, as the chunk holder is not removed during unload ++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); ++ if (into != null) { ++ into.chunkHoldersToBroadcast.add(chunkHolder); ++ } ++ } ++ } ++ }; ++ ++ public final ServerLevel world; ++ ++ private RegionizedServer.WorldLevelData tickData; ++ ++ // connections ++ public final List connections = new ArrayList<>(); ++ ++ // misc. fields ++ private boolean isHandlingTick; ++ ++ public void setHandlingTick(final boolean to) { ++ this.isHandlingTick = to; ++ } ++ ++ public boolean isHandlingTick() { ++ return this.isHandlingTick; ++ } ++ ++ // entities ++ private final List localPlayers = new ArrayList<>(); ++ private final NearbyPlayers nearbyPlayers; ++ private final ReferenceList allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); ++ private final ReferenceList loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); ++ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>(); ++ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); ++ public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker ++ public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker ++ ++ // block ticking ++ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); ++ private final LevelTicks blockLevelTicks; ++ private final LevelTicks fluidLevelTicks; ++ ++ // tile entity ticking ++ private final List pendingBlockEntityTickers = new ArrayList<>(); ++ private final List blockEntityTickers = new ArrayList<>(); ++ private boolean tickingBlockEntities; ++ ++ // time ++ private long redstoneTime = 1L; ++ ++ public long getRedstoneGameTime() { ++ return this.redstoneTime; ++ } ++ ++ public void setRedstoneGameTime(final long to) { ++ this.redstoneTime = to; ++ } ++ ++ // ticking chunks ++ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0]; ++ private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); ++ private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); ++ private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); ++ ++ // Paper/CB api hook misc ++ // don't bother to merge/split these, no point ++ // From ServerLevel ++ public boolean hasPhysicsEvent = true; // Paper ++ public boolean hasEntityMoveEvent = false; // Paper ++ // Paper start - Optimize Hoppers ++ public boolean skipPullModeEventFire = false; ++ public boolean skipPushModeEventFire = false; ++ public boolean skipHopperEvents = false; ++ // Paper end - Optimize Hoppers ++ public long lastMidTickExecute; ++ public long lastMidTickExecuteFailure; ++ // From Level ++ public boolean populating; ++ public final NeighborUpdater neighborUpdater; ++ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 ++ public boolean captureBlockStates = false; ++ public boolean captureTreeGeneration = false; ++ public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent ++ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper ++ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper ++ public List captureDrops; ++ // Paper start ++ public int wakeupInactiveRemainingAnimals; ++ public int wakeupInactiveRemainingFlying; ++ public int wakeupInactiveRemainingMonsters; ++ public int wakeupInactiveRemainingVillagers; ++ // Paper end ++ public int currentPrimedTnt = 0; // Spigot ++ @Nullable ++ @VisibleForDebug ++ public NaturalSpawner.SpawnState lastSpawnState; ++ public boolean shouldSignal = true; ++ public final Map explosionDensityCache = new HashMap<>(64, 0.25f); ++ public final PathTypeCache pathTypesByPosCache = new PathTypeCache(); ++ public final List temporaryChunkTickList = new java.util.ArrayList<>(); ++ public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>(); ++ ++ // not transient ++ public java.util.ArrayDeque redstoneUpdateInfos; ++ ++ // Mob spawning ++ public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); ++ public int catSpawnerNextTick = 0; ++ public int patrolSpawnerNextTick = 0; ++ public int phantomSpawnerNextTick = 0; ++ public int wanderingTraderTickDelay = Integer.MIN_VALUE; ++ public int wanderingTraderSpawnDelay; ++ public int wanderingTraderSpawnChance; ++ public VillageSiegeState villageSiegeState = new VillageSiegeState(); ++ ++ public static final class VillageSiegeState { ++ public boolean hasSetupSiege; ++ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; ++ public int zombiesToSpawn; ++ public int nextSpawnTime; ++ public int spawnX; ++ public int spawnY; ++ public int spawnZ; ++ } ++ // Redstone ++ public final alternate.current.wire.WireHandler wireHandler; ++ public final com.destroystokyo.paper.util.RedstoneWireTurbo turbo; ++ ++ public RegionizedWorldData(final ServerLevel world) { ++ this.world = world; ++ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true); ++ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, false); ++ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax); ++ this.nearbyPlayers = new NearbyPlayers(world); ++ this.wireHandler = new alternate.current.wire.WireHandler(world); ++ this.turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE); ++ ++ // tasks may be drained before the region ticks, so we must set up the tick data early just in case ++ this.updateTickData(); ++ } ++ ++ public void checkWorld(final Level against) { ++ if (this.world != against) { ++ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName())); ++ } ++ } ++ ++ public RegionizedServer.WorldLevelData getTickData() { ++ return this.tickData; ++ } ++ ++ private long lagCompensationTick; ++ ++ public long getLagCompensationTick() { ++ return this.lagCompensationTick; ++ } ++ ++ public void updateTickData() { ++ this.tickData = this.world.tickData; ++ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper ++ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry ++ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS; ++ } ++ ++ public NearbyPlayers getNearbyPlayers() { ++ return this.nearbyPlayers; ++ } ++ ++ private static void cleanUpConnection(final Connection conn) { ++ // note: ALL connections HERE have a player ++ final ServerPlayer player = conn.getPlayer(); ++ // now that the connection is removed, we can allow this region to die ++ player.serverLevel().chunkSource.removeTicketAtLevel( ++ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos, ++ ChunkHolderManager.MAX_TICKET_LEVEL, ++ player.connection.disconnectTicketId ++ ); ++ } ++ ++ // connections ++ public void tickConnections() { ++ final List connections = new ArrayList<>(this.connections); ++ Collections.shuffle(connections); ++ for (final Connection conn : connections) { ++ if (!conn.isConnected()) { ++ conn.handleDisconnection(); ++ // global tick thread will not remove connections not owned by it, so we need to ++ RegionizedServer.getInstance().removeConnection(conn); ++ this.connections.remove(conn); ++ cleanUpConnection(conn); ++ continue; ++ } ++ if (!this.connections.contains(conn)) { ++ // removed by connection tick? ++ continue; ++ } ++ ++ try { ++ conn.tick(); ++ } catch (final Exception exception) { ++ if (conn.isMemoryConnection()) { ++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); ++ } ++ ++ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); ++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); ++ ++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { ++ conn.disconnect(ichatmutablecomponent); ++ })); ++ conn.setReadOnly(); ++ continue; ++ } ++ } ++ } ++ ++ // entities hooks ++ public int getEntityCount() { ++ return this.allEntities.size(); ++ } ++ ++ public int getPlayerCount() { ++ return this.localPlayers.size(); ++ } ++ ++ public Iterable getLocalEntities() { ++ return this.allEntities; ++ } ++ ++ public Entity[] getLocalEntitiesCopy() { ++ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class); ++ } ++ ++ public List getLocalPlayers() { ++ return this.localPlayers; ++ } ++ ++ public void addLoadedEntity(final Entity entity) { ++ this.loadedEntities.add(entity); ++ } ++ ++ public boolean hasLoadedEntity(final Entity entity) { ++ return this.loadedEntities.contains(entity); ++ } ++ ++ public void removeLoadedEntity(final Entity entity) { ++ this.loadedEntities.remove(entity); ++ } ++ ++ public Iterable getLoadedEntities() { ++ return this.loadedEntities; ++ } ++ ++ public void addEntityTickingEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ this.entityTickList.add(entity); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public boolean hasEntityTickingEntity(final Entity entity) { ++ return this.entityTickList.contains(entity); ++ } ++ ++ public void removeEntityTickingEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ this.entityTickList.remove(entity); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void forEachTickingEntity(final Consumer action) { ++ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator(); ++ try { ++ while (iterator.hasNext()) { ++ action.accept(iterator.next()); ++ } ++ } finally { ++ iterator.finishedIterating(); ++ } ++ } ++ ++ public void addEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ if (this.allEntities.add(entity)) { ++ if (entity instanceof ServerPlayer player) { ++ this.localPlayers.add(player); ++ } ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ } ++ ++ public boolean hasEntity(final Entity entity) { ++ return this.allEntities.contains(entity); ++ } ++ ++ public void removeEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ if (this.allEntities.remove(entity)) { ++ if (entity instanceof ServerPlayer player) { ++ this.localPlayers.remove(player); ++ } ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ } ++ ++ public void addNavigatingMob(final Mob mob) { ++ if (!TickThread.isTickThreadFor(mob)) { ++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); ++ } ++ this.navigatingMobs.add(mob); ++ } ++ ++ public void removeNavigatingMob(final Mob mob) { ++ if (!TickThread.isTickThreadFor(mob)) { ++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); ++ } ++ this.navigatingMobs.remove(mob); ++ } ++ ++ public Iterator getNavigatingMobs() { ++ return this.navigatingMobs.unsafeIterator(); ++ } ++ ++ // block ticking hooks ++ // Since block event data does not require chunk holders to be created for the chunk they reside in, ++ // it's not actually guaranteed that when merging / splitting data that we actually own the data... ++ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to ++ // make the code easier by simply discarding it in such an event ++ public void pushBlockEvent(final BlockEventData blockEventData) { ++ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async"); ++ this.blockEvents.add(blockEventData); ++ } ++ ++ public void pushBlockEvents(final Collection blockEvents) { ++ for (final BlockEventData blockEventData : blockEvents) { ++ this.pushBlockEvent(blockEventData); ++ } ++ } ++ ++ public void removeIfBlockEvents(final Predicate predicate) { ++ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { ++ final BlockEventData blockEventData = iterator.next(); ++ if (predicate.test(blockEventData)) { ++ iterator.remove(); ++ } ++ } ++ } ++ ++ public BlockEventData removeFirstBlockEvent() { ++ BlockEventData ret; ++ while (!this.blockEvents.isEmpty()) { ++ ret = this.blockEvents.removeFirst(); ++ if (TickThread.isTickThreadFor(this.world, ret.pos())) { ++ return ret; ++ } // else: chunk must have been unloaded ++ } ++ ++ return null; ++ } ++ ++ public LevelTicks getBlockLevelTicks() { ++ return this.blockLevelTicks; ++ } ++ ++ public LevelTicks getFluidLevelTicks() { ++ return this.fluidLevelTicks; ++ } ++ ++ // tile entity ticking ++ public void addBlockEntityTicker(final TickingBlockEntity ticker) { ++ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); ++ ++ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ } ++ ++ public void seTtickingBlockEntities(final boolean to) { ++ this.tickingBlockEntities = true; ++ } ++ ++ public List getBlockEntityTickers() { ++ return this.blockEntityTickers; ++ } ++ ++ public void pushPendingTickingBlockEntities() { ++ if (!this.pendingBlockEntityTickers.isEmpty()) { ++ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); ++ this.pendingBlockEntityTickers.clear(); ++ } ++ } ++ ++ // ticking chunks ++ public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.entityTickingChunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.entityTickingChunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getEntityTickingChunks() { ++ return this.entityTickingChunks; ++ } ++ ++ public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.tickingChunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.tickingChunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getTickingChunks() { ++ return this.tickingChunks; ++ } ++ ++ public void addChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.chunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.chunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getChunks() { ++ return this.chunks; ++ } ++ ++ public int getEntityTickingChunkCount() { ++ return this.entityTickingChunks.size(); ++ } ++ ++ public int getChunkCount() { ++ return this.chunks.size(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java +new file mode 100644 +index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java +@@ -0,0 +1,91 @@ ++package io.papermc.paper.threadedregions; ++ ++/** ++ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. ++ */ ++public final class Schedule { ++ ++ private long lastPeriod; ++ ++ /** ++ * Initialises a schedule with the provided period. ++ * @param firstPeriod The last time an event of interest occurred. ++ * @see #setLastPeriod(long) ++ */ ++ public Schedule(final long firstPeriod) { ++ this.lastPeriod = firstPeriod; ++ } ++ ++ /** ++ * Updates the last period to the specified value. This call sets the last "time" the event ++ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is ++ * the provided time plus the period length provided to {@code getDeadline}. ++ * @param value The value to set the last period to. ++ */ ++ public void setLastPeriod(final long value) { ++ this.lastPeriod = value; ++ } ++ ++ /** ++ * Returns the last time the event of interest should have taken place. ++ */ ++ public long getLastPeriod() { ++ return this.lastPeriod; ++ } ++ ++ /** ++ * Returns the number of times the event of interest should have taken place between the last ++ * period and the provided time given the period between each event. ++ * @param periodLength The length of the period between events in ns. ++ * @param time The provided time. ++ */ ++ public int getPeriodsAhead(final long periodLength, final long time) { ++ final long difference = time - this.lastPeriod; ++ final int ret = (int)(Math.abs(difference) / periodLength); ++ return difference >= 0 ? ret : -ret; ++ } ++ ++ /** ++ * Returns the next starting deadline for the event of interest to take place, ++ * given the provided period length. ++ * @param periodLength The provided period length. ++ */ ++ public long getDeadline(final long periodLength) { ++ return this.lastPeriod + periodLength; ++ } ++ ++ /** ++ * Adjusts the last period so that the next starting deadline returned is the next period specified, ++ * given the provided period length. ++ * @param nextPeriod The specified next starting deadline. ++ * @param periodLength The specified period length. ++ */ ++ public void setNextPeriod(final long nextPeriod, final long periodLength) { ++ this.lastPeriod = nextPeriod - periodLength; ++ } ++ ++ /** ++ * Increases the last period by the specified number of periods and period length. ++ * The specified number of periods may be < 0, in which case the last period ++ * will decrease. ++ * @param periods The specified number of periods. ++ * @param periodLength The specified period length. ++ */ ++ public void advanceBy(final int periods, final long periodLength) { ++ this.lastPeriod += (long)periods * periodLength; ++ } ++ ++ /** ++ * Sets the last period so that it is the specified number of periods ahead ++ * given the specified time and period length. ++ * @param periodsToBeAhead Specified number of periods to be ahead by. ++ * @param periodLength The specified period length. ++ * @param time The specified time. ++ */ ++ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { ++ final int periodsAhead = this.getPeriodsAhead(periodLength, time); ++ final int periodsToAdd = periodsToBeAhead - periodsAhead; ++ ++ this.lastPeriod -= (long)periodsToAdd * periodLength; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..74ac328bf8d5f762f7060a6c5d49089dee1ddaea +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java +@@ -0,0 +1,70 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.phys.Vec3; ++import org.bukkit.Location; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.event.player.PlayerTeleportEvent; ++import java.util.function.Consumer; ++ ++public final class TeleportUtils { ++ ++ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, ++ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { ++ // retrieve coordinates ++ final CallbackCompletable positionCompletable = new CallbackCompletable<>(); ++ ++ positionCompletable.addWaiter( ++ (final Location loc, final Throwable thr) -> { ++ if (loc == null) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ return; ++ } ++ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( ++ (final Entity realFrom) -> { ++ final Vec3 pos = new Vec3( ++ loc.getX(), loc.getY(), loc.getZ() ++ ); ++ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync( ++ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, ++ cause, teleportFlags, onComplete ++ ); ++ }, ++ (final Entity retired) -> { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ ); ++ ++ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( ++ (final Entity target) -> { ++ positionCompletable.complete(target.getBukkitEntity().getLocation()); ++ }, ++ (final Entity retired) -> { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ ++ private TeleportUtils() {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8e1b1df1c889d9235b10b86fc4cedbc06b7885c2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java +@@ -0,0 +1,1405 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import com.destroystokyo.paper.util.SneakyThrow; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.StampedLock; ++import java.util.function.BooleanSupplier; ++import java.util.function.Consumer; ++ ++public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public final int regionSectionChunkSize; ++ public final int sectionChunkShift; ++ public final int minSectionRecalcCount; ++ public final int emptySectionCreateRadius; ++ public final int regionSectionMergeRadius; ++ public final double maxDeadRegionPercent; ++ public final ServerLevel world; ++ ++ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); ++ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); ++ private final RegionCallbacks callbacks; ++ private final StampedLock regionLock = new StampedLock(); ++ private Thread writeLockOwner; ++ ++ /* ++ static final record Operation(String type, int chunkX, int chunkZ) {} ++ private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); ++ */ ++ ++ /* ++ * See REGION_LOGIC.md for complete details on what this class is doing ++ */ ++ ++ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent, ++ final int emptySectionCreateRadius, final int regionSectionMergeRadius, ++ final int regionSectionChunkShift, final ServerLevel world, ++ final RegionCallbacks callbacks) { ++ if (emptySectionCreateRadius <= 0) { ++ throw new IllegalStateException("Region section create radius must be > 0"); ++ } ++ if (regionSectionMergeRadius <= 0) { ++ throw new IllegalStateException("Region section merge radius must be > 0"); ++ } ++ this.regionSectionChunkSize = 1 << regionSectionChunkShift; ++ this.sectionChunkShift = regionSectionChunkShift; ++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); ++ this.maxDeadRegionPercent = maxDeadRegionPercent; ++ this.emptySectionCreateRadius = emptySectionCreateRadius; ++ this.regionSectionMergeRadius = regionSectionMergeRadius; ++ this.world = world; ++ this.callbacks = callbacks; ++ //this.loadTestData(); ++ } ++ ++ /* ++ private static String substr(String val, String prefix, int from) { ++ int idx = val.indexOf(prefix, from) + prefix.length(); ++ int idx2 = val.indexOf(',', idx); ++ if (idx2 == -1) { ++ idx2 = val.indexOf(']', idx); ++ } ++ return val.substring(idx, idx2); ++ } ++ ++ private void loadTestData() { ++ if (true) { ++ return; ++ } ++ try { ++ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); ++ ++ List ops = new ArrayList<>(); ++ ++ for (JsonElement elem : arr) { ++ JsonObject obj = elem.getAsJsonObject(); ++ String val = obj.get("value").getAsString(); ++ ++ String type = substr(val, "type=", 0); ++ String x = substr(val, "chunkX=", 0); ++ String z = substr(val, "chunkZ=", 0); ++ ++ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); ++ } ++ ++ for (Operation op : ops) { ++ switch (op.type) { ++ case "add": { ++ this.addChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "remove": { ++ this.removeChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "mark_ticking": { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); ++ break; ++ } ++ case "rel_region": { ++ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); ++ } ++ break; ++ } ++ } ++ } ++ ++ } catch (final Exception ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ */ ++ ++ public void acquireReadLock() { ++ this.regionLock.readLock(); ++ } ++ ++ public void releaseReadLock() { ++ this.regionLock.tryUnlockRead(); ++ } ++ ++ private void acquireWriteLock() { ++ final Thread currentThread = Thread.currentThread(); ++ if (this.writeLockOwner == currentThread) { ++ throw new IllegalStateException("Cannot recursively operate in the regioniser"); ++ } ++ this.regionLock.writeLock(); ++ this.writeLockOwner = currentThread; ++ } ++ ++ private void releaseWriteLock() { ++ this.writeLockOwner = null; ++ this.regionLock.tryUnlockWrite(); ++ } ++ ++ private void onRegionCreate(final ThreadedRegion region) { ++ final ThreadedRegion conflict; ++ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { ++ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); ++ } ++ } ++ ++ private void onRegionDestroy(final ThreadedRegion region) { ++ final ThreadedRegion removed = this.regionsById.remove(region.id); ++ if (removed != region) { ++ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); ++ } ++ } ++ ++ public int getSectionCoordinate(final int chunkCoordinate) { ++ return chunkCoordinate >> this.sectionChunkShift; ++ } ++ ++ public long getSectionKey(final BlockPos pos) { ++ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final ChunkPos pos) { ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final Entity entity) { ++ final ChunkPos pos = entity.chunkPosition(); ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public void computeForAllRegions(final Consumer> consumer) { ++ this.regionLock.readLock(); ++ try { ++ this.regionsById.forEachValue(consumer); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { ++ this.regionsById.forEachValue(consumer); ++ } ++ ++ public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ, ++ final Consumer>> consumer) { ++ final int shift = this.sectionChunkShift; ++ final int fromSectionX = fromChunkX >> shift; ++ final int fromSectionZ = fromChunkZ >> shift; ++ final int toSectionX = toChunkX >> shift; ++ final int toSectionZ = toChunkZ >> shift; ++ this.acquireWriteLock(); ++ try { ++ final ReferenceOpenHashSet> set = new ReferenceOpenHashSet<>(); ++ ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final ThreadedRegionSection section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ)); ++ if (section != null) { ++ set.add(section.getRegionPlain()); ++ } ++ } ++ } ++ ++ consumer.accept(set); ++ ++ return set.size(); ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegion(); ++ } ++ ++ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // try an optimistic read ++ { ++ final long readAttempt = this.regionLock.tryOptimisticRead(); ++ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); ++ final ThreadedRegion optimisticRet = ++ optimisticSection == null ? null : optimisticSection.getRegionPlain(); ++ if (this.regionLock.validate(readAttempt)) { ++ return optimisticRet; ++ } ++ } ++ ++ // failed, fall back to acquiring the lock ++ this.regionLock.readLock(); ++ try { ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegionPlain(); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ /** ++ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless ++ * addChunk has not been called for it or removeChunk has been previously called. ++ * ++ *

    ++ * Note that it is illegal to additionally call addChunk or removeChunk for the same ++ * region section in parallel. ++ *

    ++ */ ++ public void addChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a non-zero chunk count. ++ { ++ final ThreadedRegionSection existing = this.sections.get(sectionKey); ++ if (existing != null && !existing.isEmpty()) { ++ existing.addChunk(chunkX, chunkZ); ++ return; ++ } // else: just acquire the write lock ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ List> newSections = new ArrayList<>(); ++ ++ if (section == null) { ++ // no section at all ++ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); ++ this.sections.put(sectionKey, section); ++ newSections.add(section); ++ } else { ++ section.addChunk(chunkX, chunkZ); ++ } ++ // due to the fast check from above, we know the section is empty whether we needed to create it or not ++ ++ // enforce the adjacency invariant by creating / updating neighbour sections ++ final int createRadius = this.emptySectionCreateRadius; ++ final int searchRadius = createRadius + this.regionSectionMergeRadius; ++ ReferenceOpenHashSet> nearbyRegions = null; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final boolean inCreateRange = squareDistance <= createRadius; ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ if (neighbourSection != null) { ++ if (nearbyRegions == null) { ++ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); ++ } ++ nearbyRegions.add(neighbourSection.getRegionPlain()); ++ } ++ ++ if (!inCreateRange) { ++ continue; ++ } ++ ++ // we need to ensure the section exists ++ if (neighbourSection != null) { ++ // nothing else to do ++ neighbourSection.incrementNonEmptyNeighbours(); ++ continue; ++ } ++ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); ++ if (null != this.sections.put(neighbourKey, neighbourSection)) { ++ throw new IllegalStateException("Failed to insert new section"); ++ } ++ newSections.add(neighbourSection); ++ } ++ } ++ ++ if (newSections.isEmpty()) { ++ // if we didn't add any sections, then we don't need to merge any regions or create a region ++ return; ++ } ++ ++ final ThreadedRegion regionOfInterest; ++ final boolean regionOfInterestAlive; ++ if (nearbyRegions == null) { ++ // we can simply create a new region, don't have neighbours to worry about merging into ++ regionOfInterest = new ThreadedRegion<>(this); ++ regionOfInterestAlive = true; ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ regionOfInterest.onCreate(); ++ } else { ++ // need to merge the regions ++ ThreadedRegion firstUnlockedRegion = null; ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region.isTicking()) { ++ continue; ++ } ++ firstUnlockedRegion = region; ++ if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) { ++ throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion); ++ } ++ break; ++ } ++ ++ if (firstUnlockedRegion != null) { ++ regionOfInterest = firstUnlockedRegion; ++ } else { ++ regionOfInterest = new ThreadedRegion<>(this); ++ } ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ if (firstUnlockedRegion == null) { ++ regionOfInterest.onCreate(); ++ } ++ ++ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { ++ // nothing to do further, no need to merge anything ++ return; ++ } ++ ++ // we need to now tell all the other regions to merge into the region we just created, ++ // and to merge all the ones we can immediately ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region == regionOfInterest) { ++ continue; ++ } ++ ++ if (!region.killAndMergeInto(regionOfInterest)) { ++ // note: the region may already be a merge target ++ regionOfInterest.mergeIntoLater(region); ++ } ++ } ++ ++ if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) { ++ // we need to retire this region if the merges added other pending merges ++ if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) { ++ firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT; ++ this.callbacks.onRegionInactive(firstUnlockedRegion); ++ } ++ } ++ ++ // need to set alive if we created it and there are no pending merges ++ regionOfInterestAlive = firstUnlockedRegion == null && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); ++ } ++ ++ if (regionOfInterestAlive) { ++ regionOfInterest.state = ThreadedRegion.STATE_READY; ++ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Should not happen on region " + this); ++ } ++ this.callbacks.onRegionActive(regionOfInterest); ++ } ++ ++ if (regionOfInterest.state == ThreadedRegion.STATE_READY) { ++ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Should not happen on region " + this); ++ } ++ } ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); ++ SneakyThrow.sneaky(throwable); ++ return; // unreachable ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ public void removeChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a chunk count > 1 ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ if (section == null) { ++ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); ++ } ++ if (!section.hasOnlyOneChunk()) { ++ // chunk will not go empty, so we don't need to acquire the lock ++ section.removeChunk(chunkX, chunkZ); ++ return; ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ section.removeChunk(chunkX, chunkZ); ++ ++ final int searchRadius = this.emptySectionCreateRadius; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ // should be non-null here always ++ neighbourSection.decrementNonEmptyNeighbours(); ++ } ++ } ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); ++ SneakyThrow.sneaky(throwable); ++ return; // unreachable ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ // must hold regionLock ++ private void onRegionRelease(final ThreadedRegion region) { ++ if (!region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); ++ } ++ ++ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); ++ ++ // is this region supposed to merge into any other region? ++ if (hasExpectingMerges) { ++ // merge the regions into this one ++ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); ++ for (final ThreadedRegion mergeFrom : expectingMergeFrom) { ++ if (!mergeFrom.killAndMergeInto(region)) { ++ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); ++ } ++ } ++ ++ if (!region.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); ++ } ++ ++ if (!region.mergeIntoLater.isEmpty()) { ++ // There is another nearby ticking region that we need to merge into ++ region.state = ThreadedRegion.STATE_TRANSIENT; ++ this.callbacks.onRegionInactive(region); ++ // return to avoid removing dead sections or splitting, these actions will be performed ++ // by the region we merge into ++ return; ++ } ++ } ++ ++ // now check whether we need to recalculate regions ++ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() ++ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); ++ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); ++ if (removeDeadSections) { ++ // kill dead sections ++ for (final ThreadedRegionSection deadSection : region.deadSections) { ++ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); ++ ++ if (!deadSection.isEmpty()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); ++ } ++ if (deadSection.hasNonEmptyNeighbours()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); ++ } ++ if (!region.sectionByKey.remove(key, deadSection)) { ++ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); ++ } ++ if (this.sections.remove(key) != deadSection) { ++ throw new IllegalStateException("Cannot remove dead section '" + ++ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); ++ } ++ } ++ region.deadSections.clear(); ++ } ++ ++ // if we removed dead sections, we should check if the region can be split into smaller ones ++ // otherwise, the region remains alive ++ if (!removedDeadSections) { ++ // didn't remove dead sections, don't check for split ++ region.state = ThreadedRegion.STATE_READY; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ // first, we need to build copy of coordinate->section map of all sections in recalculate ++ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); ++ ++ if (recalculateSections.isEmpty()) { ++ // looks like the region's sections were all dead, and now there is no region at all ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ return; ++ } ++ ++ // merge radius is max, since recalculateSections includes the dead or empty sections ++ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); ++ ++ final List>> newRegions = new ArrayList<>(); ++ while (!recalculateSections.isEmpty()) { ++ // select any section, then BFS around it to find all of its neighbours to form a region ++ // once no more neighbours are found, the region is complete ++ final List> currRegion = new ArrayList<>(); ++ final Iterator> firstIterator = recalculateSections.values().iterator(); ++ ++ currRegion.add(firstIterator.next()); ++ firstIterator.remove(); ++ search_loop: ++ for (int idx = 0; idx < currRegion.size(); ++idx) { ++ final ThreadedRegionSection curr = currRegion.get(idx); ++ final int centerX = curr.sectionX; ++ final int centerZ = curr.sectionZ; ++ ++ // find neighbours in radius ++ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { ++ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); ++ if (section == null) { ++ continue; ++ } ++ ++ currRegion.add(section); ++ ++ if (recalculateSections.isEmpty()) { ++ // no point in searching further ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ newRegions.add(currRegion); ++ } ++ ++ // now we have split the regions into separate parts, we can split recalculate ++ ++ if (newRegions.size() == 1) { ++ // no need to split anything, we're done here ++ region.state = ThreadedRegion.STATE_READY; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ final List> newRegionObjects = new ArrayList<>(newRegions.size()); ++ for (int i = 0, len = newRegions.size(); i < len; ++i) { ++ newRegionObjects.add(new ThreadedRegion<>(this)); ++ } ++ ++ this.callbacks.preSplit(region, newRegionObjects); ++ ++ // need to split the region, so we need to kill the old one first ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ ++ // create new regions ++ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); ++ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects); ++ ++ for (int i = 0, len = newRegions.size(); i < len; i++) { ++ final List> sections = newRegions.get(i); ++ final ThreadedRegion newRegion = newRegionObjects.get(i); ++ ++ for (final ThreadedRegionSection section : sections) { ++ section.setRegionRelease(null); ++ newRegion.addSection(section); ++ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); ++ if (curr != null) { ++ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); ++ } ++ } ++ } ++ ++ region.split(newRegionsMap, newRegionsSet); ++ ++ // only after invoking data callbacks ++ ++ for (final ThreadedRegion newRegion : newRegionsSet) { ++ newRegion.state = ThreadedRegion.STATE_READY; ++ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + newRegion); ++ } ++ newRegion.onCreate(); ++ this.callbacks.onRegionActive(newRegion); ++ } ++ } ++ ++ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { ++ ++ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); ++ ++ private static final int STATE_TRANSIENT = 0; ++ private static final int STATE_READY = 1; ++ private static final int STATE_TICKING = 2; ++ private static final int STATE_DEAD = 3; ++ ++ public final long id; ++ ++ private int state; ++ ++ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); ++ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); ++ ++ public final ThreadedRegionizer regioniser; ++ ++ private final R data; ++ ++ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); ++ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); ++ ++ public ThreadedRegion(final ThreadedRegionizer regioniser) { ++ this.regioniser = regioniser; ++ this.id = REGION_ID_GENERATOR.getAndIncrement(); ++ this.state = STATE_TRANSIENT; ++ this.data = regioniser.callbacks.createNewData(this); ++ } ++ ++ public LongArrayList getOwnedSections() { ++ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); ++ if (lock) { ++ this.regioniser.regionLock.readLock(); ++ } ++ try { ++ final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); ++ ret.addAll(this.sectionByKey.keySet()); ++ ++ return ret; ++ } finally { ++ if (lock) { ++ this.regioniser.regionLock.tryUnlockRead(); ++ } ++ } ++ } ++ ++ /** ++ * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_ ++ * 'this' region. ++ */ ++ public LongIterator getOwnedSectionsUnsynchronised() { ++ return this.sectionByKey.keySet().iterator(); ++ } ++ ++ public LongArrayList getOwnedChunks() { ++ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); ++ if (lock) { ++ this.regioniser.regionLock.readLock(); ++ } ++ try { ++ final LongArrayList ret = new LongArrayList(); ++ for (final ThreadedRegionSection section : this.sectionByKey.values()) { ++ ret.addAll(section.getChunks()); ++ } ++ ++ return ret; ++ } finally { ++ if (lock) { ++ this.regioniser.regionLock.tryUnlockRead(); ++ } ++ } ++ } ++ ++ public Long getCenterSection() { ++ final LongArrayList sections = this.getOwnedSections(); ++ ++ final LongComparator comparator = (final long k1, final long k2) -> { ++ final int x1 = CoordinateUtils.getChunkX(k1); ++ final int x2 = CoordinateUtils.getChunkX(k2); ++ ++ final int z1 = CoordinateUtils.getChunkZ(x1); ++ final int z2 = CoordinateUtils.getChunkZ(x2); ++ ++ final int zCompare = Integer.compare(z1, z2); ++ if (zCompare != 0) { ++ return zCompare; ++ } ++ ++ return Integer.compare(x1, x2); ++ }; ++ ++ // note: regions don't always have a chunk section at this point, because the region may have been killed ++ if (sections.isEmpty()) { ++ return null; ++ } ++ ++ sections.sort(comparator); ++ ++ return Long.valueOf(sections.getLong(sections.size() >> 1)); ++ } ++ ++ public ChunkPos getCenterChunk() { ++ final LongArrayList chunks = this.getOwnedChunks(); ++ ++ final LongComparator comparator = (final long k1, final long k2) -> { ++ final int x1 = CoordinateUtils.getChunkX(k1); ++ final int x2 = CoordinateUtils.getChunkX(k2); ++ ++ final int z1 = CoordinateUtils.getChunkZ(k1); ++ final int z2 = CoordinateUtils.getChunkZ(k2); ++ ++ final int zCompare = Integer.compare(z1, z2); ++ if (zCompare != 0) { ++ return zCompare; ++ } ++ ++ return Integer.compare(x1, x2); ++ }; ++ chunks.sort(comparator); ++ ++ // note: regions don't always have a chunk at this point, because the region may have been killed ++ if (chunks.isEmpty()) { ++ return null; ++ } ++ ++ final long middle = chunks.getLong(chunks.size() >> 1); ++ ++ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); ++ } ++ ++ private void onCreate() { ++ this.regioniser.onRegionCreate(this); ++ this.regioniser.callbacks.onRegionCreate(this); ++ } ++ ++ private void onRemove(final boolean wasActive) { ++ if (wasActive) { ++ this.regioniser.callbacks.onRegionInactive(this); ++ } ++ this.regioniser.callbacks.onRegionDestroy(this); ++ this.regioniser.onRegionDestroy(this); ++ } ++ ++ private final boolean hasNoAliveSections() { ++ return this.deadSections.size() == this.sectionByKey.size(); ++ } ++ ++ private final double getDeadSectionPercent() { ++ return (double)this.deadSections.size() / (double)this.sectionByKey.size(); ++ } ++ ++ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { ++ if (this.data != null) { ++ this.data.split(this.regioniser, into, regions); ++ } ++ } ++ ++ boolean killAndMergeInto(final ThreadedRegion mergeTarget) { ++ if (this.state == STATE_TICKING) { ++ return false; ++ } ++ ++ this.regioniser.callbacks.preMerge(this, mergeTarget); ++ ++ this.tryKill(); ++ ++ this.mergeInto(mergeTarget); ++ ++ return true; ++ } ++ ++ private void mergeInto(final ThreadedRegion mergeTarget) { ++ if (this == mergeTarget) { ++ throw new IllegalStateException("Cannot merge a region onto itself"); ++ } ++ if (!this.isDead()) { ++ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); ++ } else if (mergeTarget.isDead()) { ++ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); ++ } ++ ++ for (final ThreadedRegionSection section : this.sectionByKey.values()) { ++ section.setRegionRelease(null); ++ mergeTarget.addSection(section); ++ } ++ for (final ThreadedRegionSection deadSection : this.deadSections) { ++ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { ++ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); ++ } ++ if (!mergeTarget.deadSections.add(deadSection)) { ++ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); ++ } ++ } ++ ++ // forward merge expectations ++ for (final ThreadedRegion region : this.expectingMergeFrom) { ++ if (!region.mergeIntoLater.remove(this)) { ++ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); ++ } ++ if (region != mergeTarget) { ++ region.mergeIntoLater(mergeTarget); ++ } ++ } ++ ++ // forward merge into ++ for (final ThreadedRegion region : this.mergeIntoLater) { ++ if (!region.expectingMergeFrom.remove(this)) { ++ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); ++ } ++ if (region != mergeTarget) { ++ mergeTarget.mergeIntoLater(region); ++ } ++ } ++ ++ // finally, merge data ++ if (this.data != null) { ++ this.data.mergeInto(mergeTarget); ++ } ++ } ++ ++ private void mergeIntoLater(final ThreadedRegion region) { ++ if (region.isDead()) { ++ throw new IllegalStateException("Trying to merge later into a dead region: " + region); ++ } ++ final boolean add1, add2; ++ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { ++ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); ++ } ++ } ++ ++ private boolean tryKill() { ++ switch (this.state) { ++ case STATE_TRANSIENT: { ++ this.state = STATE_DEAD; ++ this.onRemove(false); ++ return true; ++ } ++ case STATE_READY: { ++ this.state = STATE_DEAD; ++ this.onRemove(true); ++ return true; ++ } ++ case STATE_TICKING: { ++ return false; ++ } ++ case STATE_DEAD: { ++ throw new IllegalStateException("Already dead"); ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + this.state); ++ } ++ } ++ } ++ ++ private boolean isDead() { ++ return this.state == STATE_DEAD; ++ } ++ ++ private boolean isTicking() { ++ return this.state == STATE_TICKING; ++ } ++ ++ private void removeDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.remove(section); ++ } ++ ++ private void addDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.add(section); ++ } ++ ++ private void addSection(final ThreadedRegionSection section) { ++ if (section.getRegionPlain() != null) { ++ throw new IllegalStateException("Section already has region"); ++ } ++ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { ++ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); ++ } ++ section.setRegionRelease(this); ++ } ++ ++ public R getData() { ++ return this.data; ++ } ++ ++ public boolean tryMarkTicking(final BooleanSupplier abort) { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_READY || abort.getAsBoolean()) { ++ return false; ++ } ++ ++ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + this + " should not be ready"); ++ } ++ ++ this.state = STATE_TICKING; ++ return true; ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ public boolean markNotTicking() { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_TICKING) { ++ throw new IllegalStateException("Attempting to release non-locked state"); ++ } ++ ++ this.regioniser.onRegionRelease(this); ++ ++ return this.state == STATE_READY; ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to release region " + this, throwable); ++ SneakyThrow.sneaky(throwable); ++ return false; // unreachable ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(128); ++ ++ ret.append("ThreadedRegion{"); ++ ret.append("state=").append(this.state).append(','); ++ // To avoid recursion in toString, maybe fix later? ++ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); ++ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); ++ ++ ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); ++ ret.append("sections=["); ++ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { ++ final ThreadedRegionSection section = iterator.next(); ++ ++ ret.append(section.toString()); ++ if (iterator.hasNext()) { ++ ret.append(','); ++ } ++ } ++ ret.append(']'); ++ ++ ret.append('}'); ++ return ret.toString(); ++ } ++ } ++ ++ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { ++ ++ public final int sectionX; ++ public final int sectionZ; ++ public final long sectionKey; ++ private final long[] chunksBitset; ++ private int chunkCount; ++ private int nonEmptyNeighbours; ++ ++ private ThreadedRegion region; ++ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); ++ ++ public final ThreadedRegionizer regioniser; ++ ++ private final int regionChunkShift; ++ private final int regionChunkMask; ++ ++ private final S data; ++ ++ private ThreadedRegion getRegionPlain() { ++ return (ThreadedRegion)REGION_HANDLE.get(this); ++ } ++ ++ private ThreadedRegion getRegionAcquire() { ++ return (ThreadedRegion)REGION_HANDLE.getAcquire(this); ++ } ++ ++ private void setRegionRelease(final ThreadedRegion value) { ++ REGION_HANDLE.setRelease(this, value); ++ } ++ ++ // creates an empty section with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) { ++ this.sectionX = sectionX; ++ this.sectionZ = sectionZ; ++ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; ++ this.regioniser = regioniser; ++ this.regionChunkShift = regioniser.sectionChunkShift; ++ this.regionChunkMask = regioniser.regionSectionChunkSize - 1; ++ this.data = regioniser.callbacks ++ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); ++ } ++ ++ // creates a section with an initial chunk with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, ++ final int chunkXInit, final int chunkZInit) { ++ this(sectionX, sectionZ, regioniser); ++ ++ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); ++ this.chunkCount = 1; ++ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE ++ } ++ ++ // creates an empty section with the specified number of non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, ++ final int nonEmptyNeighbours) { ++ this(sectionX, sectionZ, regioniser); ++ ++ this.nonEmptyNeighbours = nonEmptyNeighbours; ++ } ++ ++ public LongArrayList getChunks() { ++ final LongArrayList ret = new LongArrayList(); ++ ++ if (this.chunkCount == 0) { ++ return ret; ++ } ++ ++ final int shift = this.regionChunkShift; ++ final int mask = this.regionChunkMask; ++ final int offsetX = this.sectionX << shift; ++ final int offsetZ = this.sectionZ << shift; ++ ++ final long[] bitset = this.chunksBitset; ++ for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) { ++ long value = bitset[arrIdx]; ++ ++ for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) { ++ final int valueIdx = Long.numberOfTrailingZeros(value); ++ value ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(value); ++ ++ final int idx = valueIdx | (arrIdx << 6); ++ ++ final int localX = idx & mask; ++ final int localZ = (idx >>> shift) & mask; ++ ++ ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ private boolean isEmpty() { ++ return this.chunkCount == 0; ++ } ++ ++ private boolean hasOnlyOneChunk() { ++ return this.chunkCount == 1; ++ } ++ ++ public boolean hasNonEmptyNeighbours() { ++ return this.nonEmptyNeighbours != 0; ++ } ++ ++ /** ++ * Returns the section data associated with this region section. May be {@code null}. ++ */ ++ public S getData() { ++ return this.data; ++ } ++ ++ /** ++ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. ++ */ ++ public ThreadedRegion getRegion() { ++ return this.getRegionAcquire(); ++ } ++ ++ private int getChunkIndex(final int chunkX, final int chunkZ) { ++ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); ++ } ++ ++ private void markAlive() { ++ this.getRegionPlain().removeDeadSection(this); ++ } ++ ++ private void markDead() { ++ this.getRegionPlain().addDeadSection(this); ++ } ++ ++ private void incrementNonEmptyNeighbours() { ++ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { ++ this.markAlive(); ++ } ++ final int createRadius = this.regioniser.emptySectionCreateRadius; ++ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { ++ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); ++ } ++ } ++ ++ private void decrementNonEmptyNeighbours() { ++ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { ++ this.markDead(); ++ } ++ if (this.nonEmptyNeighbours < 0) { ++ throw new IllegalStateException("Non empty neighbours reached zero"); ++ } ++ } ++ ++ /** ++ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create ++ * dead sections / increase non-empty neighbour count for neighbouring sections. ++ */ ++ private boolean addChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); ++ if (after == bitset) { ++ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean notEmpty = ++this.chunkCount == 1; ++ if (notEmpty && this.nonEmptyNeighbours == 0) { ++ this.markAlive(); ++ } ++ return notEmpty; ++ } ++ ++ /** ++ * Returns whether the chunk count is now zero. Effectively returns whether ++ * the caller needs to decrement the neighbour count for neighbouring sections. ++ */ ++ private boolean removeChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); ++ if (before == bitset) { ++ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean empty = --this.chunkCount == 0; ++ if (empty && this.nonEmptyNeighbours == 0) { ++ this.markDead(); ++ } ++ return empty; ++ } ++ ++ @Override ++ public String toString() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "hash=" + this.hashCode() + ++ "}"; ++ } ++ ++ public String toStringWithRegion() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "hash=" + this.hashCode() + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "region=" + this.getRegionAcquire() + ++ "}"; ++ } ++ ++ private static String toString(final long[] array) { ++ final StringBuilder ret = new StringBuilder(); ++ final char[] zeros = new char[Long.SIZE / 4]; ++ for (final long value : array) { ++ // zero pad the hex string ++ Arrays.fill(zeros, '0'); ++ final String string = Long.toHexString(value); ++ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); ++ ++ ret.append(zeros); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Splits this region data into the specified regions set. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param regioniser Regioniser for which the regions reside in. ++ * @param into A map of region section coordinate key to the region that owns the section. ++ * @param regions The set of regions to split into. ++ */ ++ public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions); ++ ++ /** ++ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined ++ * except that its region data is already created. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param into Specified region. ++ */ ++ public void mergeInto(final ThreadedRegion into); ++ } ++ ++ public static interface ThreadedRegionSectionData {} ++ ++ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Creates new section data for the specified section x and section z. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param sectionX x coordinate of the section. ++ * @param sectionZ z coordinate of the section. ++ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that ++ * produces a section coordinate. ++ * @return New section data, may be {@code null}. ++ */ ++ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); ++ ++ /** ++ * Creates new region data for the specified region. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param forRegion The region to create the data for. ++ * @return New region data, may be {@code null}. ++ */ ++ public R createNewData(final ThreadedRegion forRegion); ++ ++ /** ++ * Callback for when a region is created. This is invoked after the region is completely set up, ++ * so its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that was created. ++ */ ++ public void onRegionCreate(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so ++ * its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is about to be destroyed. ++ */ ++ public void onRegionDestroy(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is considered "active." An active region x is a non-destroyed region which ++ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are ++ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any ++ * other region considering the regioniser's empty section radius. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now active. ++ */ ++ public void onRegionActive(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but ++ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, ++ * an inactive region may not tick and needs to be merged into its neighbouring regions. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now inactive. ++ */ ++ public void onRegionInactive(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region (from) is about to be merged into a target region (into). Note that ++ * {@code from} is still alive and is a distinct region. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param from The region that will be merged into the target. ++ * @param into The target of the merge. ++ */ ++ public void preMerge(final ThreadedRegion from, final ThreadedRegion into); ++ ++ /** ++ * Callback for when a region (from) is about to be split into a list of target region (into). Note that ++ * {@code from} is still alive, while the list of target regions are not initialised. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param from The region that will be merged into the target. ++ * @param into The list of regions to split into. ++ */ ++ public void preSplit(final ThreadedRegion from, final List> into); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickData.java b/src/main/java/io/papermc/paper/threadedregions/TickData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickData.java +@@ -0,0 +1,333 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import io.papermc.paper.util.IntervalledCounter; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++ ++public final class TickData { ++ ++ private final long interval; // ns ++ ++ private final ArrayDeque timeData = new ArrayDeque<>(); ++ ++ public TickData(final long intervalNS) { ++ this.interval = intervalNS; ++ } ++ ++ public void addDataFrom(final TickRegionScheduler.TickTime time) { ++ final long start = time.tickStart(); ++ ++ TickRegionScheduler.TickTime first; ++ while ((first = this.timeData.peekFirst()) != null) { ++ // only remove data completely out of window ++ if ((start - first.tickEnd()) <= this.interval) { ++ break; ++ } ++ this.timeData.pollFirst(); ++ } ++ ++ this.timeData.add(time); ++ } ++ ++ // fromIndex inclusive, toIndex exclusive ++ // will throw if arr.length == 0 ++ private static double median(final long[] arr, final int fromIndex, final int toIndex) { ++ final int len = toIndex - fromIndex; ++ final int middle = fromIndex + (len >>> 1); ++ if ((len & 1) == 0) { ++ // even, average the two middle points ++ return (double)(arr[middle - 1] + arr[middle]) / 2.0; ++ } else { ++ // odd, just grab the middle ++ return (double)arr[middle]; ++ } ++ } ++ ++ // will throw if arr.length == 0 ++ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, ++ final boolean inverse) { ++ final int len = toIndex - fromIndex; ++ long sum = 0L; ++ final double median = median(arr, fromIndex, toIndex); ++ long min = arr[0]; ++ long max = arr[0]; ++ ++ for (int i = fromIndex; i < toIndex; ++i) { ++ final long val = arr[i]; ++ sum += val; ++ if (val < min) { ++ min = val; ++ } ++ if (val > max) { ++ max = val; ++ } ++ } ++ ++ if (inverse) { ++ // for positive a,b we have that a >= b if and only if 1/a <= 1/b ++ return new SegmentData( ++ len, ++ (double)len / ((double)sum / 1.0E9), ++ 1.0E9 / median, ++ 1.0E9 / (double)max, ++ 1.0E9 / (double)min ++ ); ++ } else { ++ return new SegmentData( ++ len, ++ (double)sum / (double)len, ++ median, ++ (double)min, ++ (double)max ++ ); ++ } ++ } ++ ++ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, ++ final int percent99BestStart, final int percent99BestEnd, ++ final int percent95BestStart, final int percent95BestEnd, ++ final int percent1WorstStart, final int percent1WorstEnd, ++ final int percent5WorstStart, final int percent5WorstEnd, ++ final boolean inverse) { ++ return new SegmentedAverage( ++ computeSegmentData(data, allStart, allEnd, inverse), ++ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), ++ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), ++ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), ++ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) ++ ); ++ } ++ ++ private static record TickInformation( ++ long differenceFromLastTick, ++ long tickTime, ++ long tickTimeCPU ++ ) {} ++ ++ // rets null if there is no data ++ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { ++ if (this.timeData.isEmpty() && inProgress == null) { ++ return null; ++ } ++ ++ final List allData = new ArrayList<>(this.timeData); ++ if (inProgress != null) { ++ allData.add(inProgress); ++ } ++ ++ final long intervalStart = allData.get(0).tickStart(); ++ final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); ++ ++ // to make utilisation accurate, we need to take the total time used over the last interval period - ++ // this means if a tick start before the measurement interval, but ends within the interval, then we ++ // only consider the time it spent ticking inside the interval ++ long totalTimeOverInterval = 0L; ++ long measureStart = endTime - this.interval; ++ ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { ++ final long diff = time.tickEnd() - measureStart; ++ if (diff > 0L) { ++ totalTimeOverInterval += diff; ++ } // else: the time is entirely out of interval ++ } else { ++ totalTimeOverInterval += time.tickLength(); ++ } ++ } ++ ++ // we only care about ticks, but because of inbetween tick task execution ++ // there will be data in allData that isn't ticks. But, that data cannot ++ // be ignored since it contributes to utilisation. ++ // So, we will "compact" the data by merging any inbetween tick times ++ // the next tick. ++ // If there is no "next tick", then we will create one. ++ final List collapsedData = new ArrayList<>(); ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final List toCollapse = new ArrayList<>(); ++ TickRegionScheduler.TickTime lastTick = null; ++ for (;i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (!time.isTickExecution()) { ++ toCollapse.add(time); ++ continue; ++ } ++ lastTick = time; ++ break; ++ } ++ ++ if (toCollapse.isEmpty()) { ++ // nothing to collapse ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength(), ++ last.supportCPUTime() ? last.tickCpuTime() : 0L ++ ) ++ ); ++ } else { ++ long totalTickTime = 0L; ++ long totalCpuTime = 0L; ++ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { ++ final TickRegionScheduler.TickTime time = toCollapse.get(k); ++ totalTickTime += time.tickLength(); ++ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; ++ } ++ if (i < len) { ++ // we know there is a tick to collapse into ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength() + totalTickTime, ++ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime ++ ) ++ ); ++ } else { ++ // we do not have a tick to collapse into, so we must make one up ++ // we will assume that the tick is "starting now" and ongoing ++ ++ // compute difference between imaginary tick and last tick ++ final long differenceBetweenTicks; ++ if (lastTick != null) { ++ // we have a last tick, use it ++ differenceBetweenTicks = lastTick.tickStart(); ++ } else { ++ // we don't have a last tick, so we must make one up that makes sense ++ // if the current interval exceeds the max tick time, then use it ++ ++ // Otherwise use the interval length. ++ // This is how differenceFromLastTick() works on TickTime when there is no previous interval. ++ differenceBetweenTicks = Math.max( ++ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime ++ ); ++ } ++ ++ collapsedData.add( ++ new TickInformation( ++ differenceBetweenTicks, ++ totalTickTime, ++ totalCpuTime ++ ) ++ ); ++ } ++ } ++ } ++ ++ ++ final int collectedTicks = collapsedData.size(); ++ final long[] tickStartToStartDifferences = new long[collectedTicks]; ++ final long[] timePerTickDataRaw = new long[collectedTicks]; ++ final long[] missingCPUTimeDataRaw = new long[collectedTicks]; ++ ++ long totalTimeTicking = 0L; ++ ++ int i = 0; ++ for (final TickInformation time : collapsedData) { ++ tickStartToStartDifferences[i] = time.differenceFromLastTick(); ++ final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); ++ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); ++ ++ ++i; ++ ++ totalTimeTicking += timePerTick; ++ } ++ ++ Arrays.sort(tickStartToStartDifferences); ++ Arrays.sort(timePerTickDataRaw); ++ Arrays.sort(missingCPUTimeDataRaw); ++ ++ // Note: computeSegmentData cannot take start == end ++ final int allStart = 0; ++ final int allEnd = collectedTicks; ++ final int percent95BestStart = 0; ++ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); ++ final int percent99BestStart = 0; ++ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end ++ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); ++ final int percent1WorstStart = (int)(0.99 * collectedTicks); ++ final int percent1WorstEnd = collectedTicks; ++ final int percent5WorstStart = (int)(0.95 * collectedTicks); ++ final int percent5WorstEnd = collectedTicks; ++ ++ final SegmentedAverage tpsData = computeSegmentedAverage( ++ tickStartToStartDifferences, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ true ++ ); ++ ++ final SegmentedAverage timePerTickData = computeSegmentedAverage( ++ timePerTickDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( ++ missingCPUTimeDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final double utilisation = (double)totalTimeOverInterval / (double)this.interval; ++ ++ return new TickReportData( ++ collectedTicks, ++ intervalStart, ++ intervalEnd, ++ totalTimeTicking, ++ utilisation, ++ ++ tpsData, ++ timePerTickData, ++ missingCPUTimeData ++ ); ++ } ++ ++ public static final record TickReportData( ++ int collectedTicks, ++ long collectedTickIntervalStart, ++ long collectedTickIntervalEnd, ++ long totalTimeTicking, ++ double utilisation, ++ ++ SegmentedAverage tpsData, ++ // in ns ++ SegmentedAverage timePerTickData, ++ // in ns ++ SegmentedAverage missingCPUTimeData ++ ) {} ++ ++ public static final record SegmentedAverage( ++ SegmentData segmentAll, ++ SegmentData segment99PercentBest, ++ SegmentData segment95PercentBest, ++ SegmentData segment5PercentWorst, ++ SegmentData segment1PercentWorst ++ ) {} ++ ++ public static final record SegmentData( ++ int count, ++ double average, ++ double median, ++ double least, ++ double greatest ++ ) {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4471285a4358e51da9912ed791a824527f1a2e8e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -0,0 +1,564 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.TraceUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.lang.management.ManagementFactory; ++import java.lang.management.ThreadMXBean; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegionScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); ++ private static final boolean MEASURE_CPU_TIME; ++ static { ++ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); ++ if (MEASURE_CPU_TIME) { ++ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); ++ } else { ++ LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); ++ } ++ } ++ ++ public static final int TICK_RATE = 20; ++ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns ++ ++ private final SchedulerThreadPool scheduler; ++ ++ public TickRegionScheduler(final int threads) { ++ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); ++ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); ++ return ret; ++ } ++ }); ++ } ++ ++ public int getTotalThreadCount() { ++ return this.scheduler.getThreads().length; ++ } ++ ++ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (region != null && tickThreadRunner.currentTickingRegion != null) { ++ throw new IllegalStateException("Trying to double set ticking region!"); ++ } ++ if (region == null && tickThreadRunner.currentTickingRegion == null) { ++ throw new IllegalStateException("Trying to double unset ticking region!"); ++ } ++ tickThreadRunner.currentTickingRegion = region; ++ if (region != null) { ++ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get(); ++ } else { ++ tickThreadRunner.currentTickingWorldRegionizedData = null; ++ } ++ } ++ ++ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (task != null && tickThreadRunner.currentTickingTask != null) { ++ throw new IllegalStateException("Trying to double set ticking task!"); ++ } ++ if (task == null && tickThreadRunner.currentTickingTask == null) { ++ throw new IllegalStateException("Trying to double unset ticking task!"); ++ } ++ tickThreadRunner.currentTickingTask = task; ++ } ++ ++ /** ++ * Returns the current ticking region, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getRegion(); ++ } ++ return tickThreadRunner.currentTickingRegion; ++ } ++ ++ /** ++ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. ++ * This is a faster alternative to calling the {@link RegionizedData#get()} method. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static RegionizedWorldData getCurrentRegionizedWorldData() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getWorldData(); ++ } ++ return tickThreadRunner.currentTickingWorldRegionizedData; ++ } ++ ++ /** ++ * Returns the current ticking task, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return null; ++ } ++ return tickThreadRunner.currentTickingTask; ++ } ++ ++ /** ++ * Schedules the given region ++ * @throws IllegalStateException If the region is already scheduled or is ticking ++ */ ++ public void scheduleRegion(final RegionScheduleHandle region) { ++ region.scheduler = this; ++ this.scheduler.schedule(region); ++ } ++ ++ /** ++ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task ++ * execution, then it will be cancelled after. ++ */ ++ public void descheduleRegion(final RegionScheduleHandle region) { ++ // To avoid acquiring any of the locks the scheduler may be using, we ++ // simply cancel the next action. ++ region.markNonSchedulable(); ++ } ++ ++ /** ++ * Updates the tick start to the farthest into the future of its current scheduled time and the ++ * provided time. ++ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its ++ * current start time, {@code true} if the next tick start was adjusted. ++ */ ++ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { ++ return this.scheduler.updateTickStartToMax(region, newStart); ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ return this.scheduler.halt(sync, maxWaitNS); ++ } ++ ++ void dumpAliveThreadTraces(final String reason) { ++ for (final Thread thread : this.scheduler.getThreads()) { ++ if (thread.isAlive()) { ++ TraceUtil.dumpTraceForThread(thread, reason); ++ } ++ } ++ } ++ ++ public void setHasTasks(final RegionScheduleHandle region) { ++ this.scheduler.notifyTasks(region); ++ } ++ ++ public void init() { ++ this.scheduler.start(); ++ } ++ ++ private void uncaughtException(final Thread thread, final Throwable thr) { ++ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr); ++ ++ // prevent further ticks from occurring ++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD ++ this.scheduler.halt(false, 0L); ++ ++ MinecraftServer.getServer().stopServer(); ++ } ++ ++ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { ++ // when a region fails, we need to shut down the server gracefully ++ ++ // prevent further ticks from occurring ++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD ++ this.scheduler.halt(false, 0L); ++ ++ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk(); ++ final ServerLevel world = handle.region == null ? null : handle.region.world; ++ ++ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); ++ ++ MinecraftServer.getServer().stopServer(); ++ } ++ ++ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. ++ // This is much faster than a thread local, since the thread local has to use a map lookup. ++ private static final class TickThreadRunner extends TickThread { ++ ++ private ThreadedRegionizer.ThreadedRegion currentTickingRegion; ++ private RegionizedWorldData currentTickingWorldRegionizedData; ++ private SchedulerThreadPool.SchedulableTick currentTickingTask; ++ ++ public TickThreadRunner(final Runnable run, final String name) { ++ super(run, name); ++ } ++ } ++ ++ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { ++ ++ protected long currentTick; ++ protected long lastTickStart; ++ ++ protected final TickData tickTimes5s; ++ protected final TickData tickTimes15s; ++ protected final TickData tickTimes1m; ++ protected final TickData tickTimes5m; ++ protected final TickData tickTimes15m; ++ protected TickTime currentTickData; ++ protected Thread currentTickingThread; ++ ++ public final TickRegions.TickRegionData region; ++ private final AtomicBoolean cancelled = new AtomicBoolean(); ++ ++ protected final Schedule tickSchedule; ++ ++ private TickRegionScheduler scheduler; ++ ++ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { ++ this.currentTick = 0L; ++ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; ++ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); ++ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); ++ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); ++ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); ++ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); ++ this.region = region; ++ ++ this.setScheduledStart(firstStart); ++ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); ++ } ++ ++ /** ++ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} ++ * so that the tick schedule and scheduled start remain synchronised ++ */ ++ protected final void updateScheduledStart(final long to) { ++ this.setScheduledStart(to); ++ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); ++ } ++ ++ public final void markNonSchedulable() { ++ this.cancelled.set(true); ++ } ++ ++ public final boolean isMarkedAsNonSchedulable() { ++ return this.cancelled.get(); ++ } ++ ++ protected abstract boolean tryMarkTicking(); ++ ++ protected abstract boolean markNotTicking(); ++ ++ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); ++ ++ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); ++ ++ protected abstract boolean hasIntermediateTasks(); ++ ++ @Override ++ public final boolean hasTasks() { ++ return this.hasIntermediateTasks(); ++ } ++ ++ @Override ++ public final Boolean runTasks(final BooleanSupplier canContinue) { ++ if (this.cancelled.get()) { ++ return null; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return null; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ false ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ final boolean ret; ++ try { ++ ret = this.runRegionTasks(() -> { ++ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); ++ }); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, true, thr); ++ // don't release region for another tick ++ return null; ++ } finally { ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ final TickTime time = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, ++ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false ++ ); ++ ++ this.addTickTime(time); ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ } ++ ++ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); ++ } ++ ++ @Override ++ public final boolean runTick() { ++ // Remember, we are supposed use setScheduledStart if we return true here, otherwise ++ // the scheduler will try to schedule for the same time. ++ if (this.cancelled.get()) { ++ return false; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ // use max(), don't assume that tickStart >= scheduledStart ++ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return false; ++ } ++ if (this.cancelled.get()) { ++ this.markNotTicking(); ++ // region should be killed ++ return false; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ this.incrementTickCount(); ++ final long lastTickStart = this.lastTickStart; ++ this.lastTickStart = tickStart; ++ ++ final long scheduledStart = this.getScheduledStart(); ++ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ true ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ try { ++ // next start isn't updated until the end of this tick ++ this.tickRegion(tickCount, tickStart, scheduledEnd); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, false, thr); ++ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further ++ return false; ++ } finally { ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions ++ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest ++ // of the current time and "ideal" next tick start. ++ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); ++ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); ++ ++ final TickTime time = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true ++ ); ++ ++ this.addTickTime(time); ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ } ++ ++ // Only AFTER updating the tickStart ++ return this.markNotTicking() && !this.cancelled.get(); ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ protected void addTickTime(final TickTime time) { ++ synchronized (this) { ++ this.currentTickData = null; ++ this.currentTickingThread = null; ++ this.tickTimes5s.addDataFrom(time); ++ this.tickTimes15s.addDataFrom(time); ++ this.tickTimes1m.addDataFrom(time); ++ this.tickTimes5m.addDataFrom(time); ++ this.tickTimes15m.addDataFrom(time); ++ } ++ } ++ ++ private TickTime adjustCurrentTickData(final long tickEnd) { ++ final TickTime currentTickData = this.currentTickData; ++ if (currentTickData == null) { ++ return null; ++ } ++ ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; ++ ++ return new TickTime( ++ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), ++ currentTickData.tickStart(), currentTickData.tickStartCPU(), ++ tickEnd, cpuEnd, ++ MEASURE_CPU_TIME, currentTickData.isTickExecution() ++ ); ++ } ++ ++ public final TickData.TickReportData getTickReport5s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport1m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport5m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ private void incrementTickCount() { ++ ++this.currentTick; ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ public final long getCurrentTick() { ++ return this.currentTick; ++ } ++ ++ protected final void setCurrentTick(final long value) { ++ this.currentTick = value; ++ } ++ } ++ ++ // All time units are in nanoseconds. ++ public static final record TickTime( ++ long previousTickStart, ++ long scheduledTickStart, ++ long tickStart, ++ long tickStartCPU, ++ long tickEnd, ++ long tickEndCPU, ++ boolean supportCPUTime, ++ boolean isTickExecution ++ ) { ++ /** ++ * The difference between the start tick time and the scheduled start tick time. This value is ++ * < 0 if the tick started before the scheduled tick time. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long startOvershoot() { ++ return this.tickStart - this.scheduledTickStart; ++ } ++ ++ /** ++ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). ++ */ ++ public final long tickLength() { ++ return this.tickEnd - this.tickStart; ++ } ++ ++ /** ++ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, ++ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE ++ * if CPU time measurement is not supported. ++ */ ++ public final long tickCpuTime() { ++ if (!this.supportCPUTime()) { ++ return Long.MIN_VALUE; ++ } ++ return this.tickEndCPU - this.tickStartCPU; ++ } ++ ++ /** ++ * The difference in time from the start of the last tick to the start of the current tick. If there is no ++ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long differenceFromLastTick() { ++ if (this.hasLastTick()) { ++ return this.tickStart - this.previousTickStart; ++ } ++ return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); ++ } ++ ++ /** ++ * Returns whether there was a tick that occurred before this one. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public boolean hasLastTick() { ++ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; ++ } ++ ++ /* ++ * Remember, this is the expected behavior of the following: ++ * ++ * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). ++ * ++ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). ++ */ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644 +--- a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +@@ -1,10 +1,410 @@ + package io.papermc.paper.threadedregions; + +-// placeholder class for Folia +-public class TickRegions { ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import java.util.Iterator; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegions implements ThreadedRegionizer.RegionCallbacks { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static int regionShift = 31; + + public static int getRegionChunkShift() { +- return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; ++ return regionShift; ++ } ++ ++ private static boolean initialised; ++ private static TickRegionScheduler scheduler; ++ ++ public static TickRegionScheduler getScheduler() { ++ return scheduler; ++ } ++ ++ public static void init(final GlobalConfiguration.ThreadedRegions config) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ int gridExponent = config.gridExponent; ++ gridExponent = Math.max(0, gridExponent); ++ gridExponent = Math.min(31, gridExponent); ++ regionShift = gridExponent; ++ ++ int tickThreads; ++ if (config.threads <= 0) { ++ tickThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (tickThreads <= 4) { ++ tickThreads = 1; ++ } else { ++ tickThreads = tickThreads / 4; ++ } ++ } else { ++ tickThreads = config.threads; ++ } ++ ++ scheduler = new TickRegionScheduler(tickThreads); ++ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); ++ } ++ ++ @Override ++ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) { ++ return new TickRegionData(region); ++ } ++ ++ @Override ++ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { ++ return null; ++ } ++ ++ @Override ++ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ // post-region merge/split regioninfo update ++ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData)); ++ } ++ ++ @Override ++ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) { ++ // nothing for now ++ } ++ ++ @Override ++ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ data.tickHandle.checkInitialSchedule(); ++ scheduler.scheduleRegion(data.tickHandle); ++ } ++ ++ @Override ++ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ scheduler.descheduleRegion(data.tickHandle); ++ // old handle cannot be scheduled anymore, copy to a new handle ++ data.tickHandle = data.tickHandle.copy(); ++ } ++ ++ @Override ++ public void preMerge(final ThreadedRegionizer.ThreadedRegion from, ++ final ThreadedRegionizer.ThreadedRegion into) { ++ ++ } ++ ++ @Override ++ public void preSplit(final ThreadedRegionizer.ThreadedRegion from, ++ final java.util.List> into) { ++ ++ } ++ ++ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {} ++ ++ public static final class RegionStats { ++ ++ private final AtomicInteger entityCount = new AtomicInteger(); ++ private final AtomicInteger playerCount = new AtomicInteger(); ++ private final AtomicInteger chunkCount = new AtomicInteger(); ++ ++ public int getEntityCount() { ++ return this.entityCount.get(); ++ } ++ ++ public int getPlayerCount() { ++ return this.playerCount.get(); ++ } ++ ++ public int getChunkCount() { ++ return this.chunkCount.get(); ++ } ++ ++ void updateFrom(final RegionizedWorldData data) { ++ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount()); ++ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount()); ++ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount()); ++ } ++ ++ static void updateCurrentRegion() { ++ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData()); ++ } ++ } ++ ++ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ /** Never 0L, since 0L is reserved for global region. */ ++ public final long id = ID_GENERATOR.incrementAndGet(); ++ ++ public final ThreadedRegionizer.ThreadedRegion region; ++ public final ServerLevel world; ++ ++ // generic regionised data ++ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>(); ++ ++ // tick data ++ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); ++ ++ // queue data ++ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData; ++ ++ // chunk holder manager data ++ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); ++ ++ // async-safe read-only region data ++ private final RegionStats regionStats; ++ ++ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) { ++ this.region = region; ++ this.world = region.regioniser.world; ++ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); ++ this.regionStats = new RegionStats(); ++ } ++ ++ public RegionStats getRegionStats() { ++ return this.regionStats; ++ } ++ ++ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() { ++ return this.taskQueueData; ++ } ++ ++ // the value returned can be invalidated at any time, except when the caller ++ // is ticking this region ++ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { ++ return this.tickHandle; ++ } ++ ++ public long getCurrentTick() { ++ return this.tickHandle.getCurrentTick(); ++ } ++ ++ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { ++ return this.holderManagerRegionData; ++ } ++ ++ T getRegionizedData(final RegionizedData regionizedData) { ++ return (T)this.regionizedData.get(regionizedData); ++ } ++ ++ T getOrCreateRegionizedData(final RegionizedData regionizedData) { ++ T ret = (T)this.regionizedData.get(regionizedData); ++ ++ if (ret != null) { ++ return ret; ++ } ++ ++ ret = regionizedData.createNewValue(); ++ this.regionizedData.put(regionizedData, ret); ++ ++ return ret; ++ } ++ ++ @Override ++ public void split(final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions) { ++ final int shift = regioniser.sectionChunkShift; ++ ++ // tick data ++ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), ++ // and the other regions to split into are not scheduled yet. ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ final TickRegionData data = region.getData(); ++ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); ++ } ++ ++ // generic regionised data ++ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); ++ dataIterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); ++ final RegionizedData data = regionDataEntry.getKey(); ++ final Object from = regionDataEntry.getValue(); ++ ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().getOrCreateRegionizedData(data)); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); ++ final Object to = region.getData().getOrCreateRegionizedData(data); ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ ((RegionizedData)data).getCallback().split(from, shift, regionToData, dataSet); ++ } ++ ++ // chunk holder manager data ++ { ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().holderManagerRegionData); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); ++ final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ this.holderManagerRegionData.split(shift, regionToData, dataSet); ++ } ++ ++ // task queue ++ this.taskQueueData.split(regioniser, into); ++ } ++ ++ @Override ++ public void mergeInto(final ThreadedRegionizer.ThreadedRegion into) { ++ // Note: merge target is always a region being released from ticking ++ final TickRegionData data = into.getData(); ++ final long currentTickTo = data.getCurrentTick(); ++ final long currentTickFrom = this.getCurrentTick(); ++ ++ // here we can access tickHandle because the target (into) is the region being released, so it is ++ // not actually scheduled ++ // there's not really a great solution to the tick problem, no matter what it'll be messed up ++ // we will pick the greatest time delay so that tps will not exceed TICK_RATE ++ data.tickHandle.updateSchedulingToMax(this.tickHandle); ++ ++ // generic regionised data ++ final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd ++ for (final Iterator, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); ++ final RegionizedData regionizedData = entry.getKey(); ++ final Object from = entry.getValue(); ++ final Object to = into.getData().getOrCreateRegionizedData(regionizedData); ++ ++ ((RegionizedData)regionizedData).getCallback().merge(from, to, fromTickOffset); ++ } ++ ++ // chunk holder manager data ++ this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); ++ ++ // task queue ++ this.taskQueueData.mergeInto(data.taskQueueData); ++ } ++ } ++ ++ private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final TickRegionData region; ++ ++ private ConcreteRegionTickHandle(final TickRegionData region, final long start) { ++ super(region, start); ++ this.region = region; ++ } ++ ++ private ConcreteRegionTickHandle copy() { ++ final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); ++ ++ ret.currentTick = this.currentTick; ++ ret.lastTickStart = this.lastTickStart; ++ ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); ++ ++ return ret; ++ } ++ ++ private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(from.getScheduledStart()); ++ return; ++ } ++ ++ this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); ++ } ++ ++ private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { ++ this.currentTick = from.currentTick; ++ ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); ++ this.setScheduledStart(from.getScheduledStart()); ++ } ++ ++ private void checkInitialSchedule() { ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ } ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.region.region.markNotTicking(); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; ++ ++ boolean processedChunkTask = false; ++ ++ boolean executeChunkTask = true; ++ boolean executeTickTask = true; ++ do { ++ if (executeTickTask) { ++ executeTickTask = queue.executeTickTask(); ++ } ++ if (executeChunkTask) { ++ processedChunkTask |= (executeChunkTask = queue.executeChunkTask()); ++ } ++ } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); ++ ++ if (processedChunkTask) { ++ // if we processed any chunk tasks, try to process ticket level updates for full status changes ++ this.region.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); ++ } ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return this.region.taskQueueData.hasTasks(); ++ } + } + + } +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3bcb1dc98c61e025874cc9e008faa722581a530c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +@@ -0,0 +1,355 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.RegionizedWorldData; ++import io.papermc.paper.threadedregions.ThreadedRegionizer; ++import io.papermc.paper.threadedregions.TickData; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.Bukkit; ++import org.bukkit.World; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.Player; ++import java.text.DecimalFormat; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Locale; ++ ++public final class CommandServerHealth extends Command { ++ ++ private static final ThreadLocal TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0.00"); ++ }); ++ private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0.0"); ++ }); ++ private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0"); ++ }); ++ ++ private static final TextColor HEADER = TextColor.color(79, 164, 240); ++ private static final TextColor PRIMARY = TextColor.color(48, 145, 237); ++ private static final TextColor SECONDARY = TextColor.color(104, 177, 240); ++ private static final TextColor INFORMATION = TextColor.color(145, 198, 243); ++ private static final TextColor LIST = TextColor.color(33, 97, 188); ++ ++ public CommandServerHealth() { ++ super("tps"); ++ this.setUsage("/ [server/region] [lowest regions to display]"); ++ this.setDescription("Reports information about server health."); ++ this.setPermission("bukkit.command.tps"); ++ } ++ ++ private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps, ++ final boolean newline) { ++ return Component.text() ++ .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) ++ .build(); ++ } ++ ++ private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) { ++ return Component.text() ++ .append(Component.text("Chunks: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION)) ++ .append(Component.text(" Players: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION)) ++ .append(Component.text(" Entities: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION)) ++ .build(); ++ } ++ ++ private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); ++ return true; ++ } ++ ++ final long currTime = System.nanoTime(); ++ ++ final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ ++ final double util15s = report15s.utilisation(); ++ final double tps15s = report15s.tpsData().segmentAll().average(); ++ final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final double util1m = report1m.utilisation(); ++ final double tps1m = report1m.tpsData().segmentAll().average(); ++ final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ ++ final Component line = Component.text() ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append( ++ formatRegionInfo("15s: ", util15s, mspt15s, tps15s, true) ++ ) ++ .append( ++ formatRegionInfo("1m: ", util1m, mspt1m, tps1m, true) ++ ) ++ .append( ++ formatRegionStats(region.getData().getRegionStats(), false) ++ ) ++ ++ .build(); ++ ++ sender.sendMessage(line); ++ ++ return true; ++ } ++ ++ private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { ++ final int lowestRegionsCount; ++ if (args.length < 2) { ++ lowestRegionsCount = 3; ++ } else { ++ try { ++ lowestRegionsCount = Integer.parseInt(args[1]); ++ } catch (final NumberFormatException ex) { ++ sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ ++ final List> regions = ++ new ArrayList<>(); ++ ++ for (final World bukkitWorld : Bukkit.getWorlds()) { ++ final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); ++ world.regioniser.computeForAllRegions(regions::add); ++ } ++ ++ final double minTps; ++ final double medianTps; ++ final double maxTps; ++ double totalUtil = 0.0; ++ ++ final DoubleArrayList tpsByRegion = new DoubleArrayList(); ++ final List reportsByRegion = new ArrayList<>(); ++ final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); ++ ++ final long currTime = System.nanoTime(); ++ final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); ++ reportsByRegion.add(report); ++ totalUtil += (report == null ? 0.0 : report.utilisation()); ++ } ++ ++ totalUtil += globalTickReport.utilisation(); ++ ++ tpsByRegion.sort(null); ++ if (!tpsByRegion.isEmpty()) { ++ minTps = tpsByRegion.getDouble(0); ++ maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); ++ ++ final int middle = tpsByRegion.size() >> 1; ++ if ((tpsByRegion.size() & 1) == 0) { ++ // even, average the two middle points ++ medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; ++ } else { ++ // odd, can just grab middle ++ medianTps = tpsByRegion.getDouble(middle); ++ } ++ } else { ++ // no regions = green ++ minTps = medianTps = maxTps = 20.0; ++ } ++ ++ final List, TickData.TickReportData>> ++ regionsBelowThreshold = new ArrayList<>(); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ final TickData.TickReportData report = reportsByRegion.get(i); ++ ++ regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); ++ } ++ ++ regionsBelowThreshold.sort((p1, p2) -> { ++ final TickData.TickReportData report1 = p1.right(); ++ final TickData.TickReportData report2 = p2.right(); ++ final double util1 = report1 == null ? 0.0 : report1.utilisation(); ++ final double util2 = report2 == null ? 0.0 : report2.utilisation(); ++ ++ // we want the largest first ++ return Double.compare(util2, util1); ++ }); ++ ++ final TextComponent.Builder lowestRegionsBuilder = Component.text(); ++ ++ if (sender instanceof Player) { ++ lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); ++ } ++ for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { ++ final ObjectObjectImmutablePair, TickData.TickReportData> ++ pair = regionsBelowThreshold.get(i); ++ ++ final TickData.TickReportData report = pair.right(); ++ final ThreadedRegionizer.ThreadedRegion region = ++ pair.left(); ++ ++ if (report == null) { ++ // skip regions with no data ++ continue; ++ } ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ if (chunkCenter == null) { ++ // region does not exist anymore ++ continue; ++ } ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ final double util = report.utilisation(); ++ final double tps = report.tpsData().segmentAll().average(); ++ final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ final Component line = Component.text() ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append(Component.text(" ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS\n", PRIMARY)) ++ ++ .append(Component.text(" ", PRIMARY)) ++ .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len)) ++ .build() ++ ++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) ++ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); ++ ++ lowestRegionsBuilder.append(line); ++ } ++ ++ sender.sendMessage( ++ Component.text() ++ .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Online Players: ", PRIMARY)) ++ .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Total regions: ", PRIMARY)) ++ .append(Component.text(regions.size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Utilisation: ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) ++ .append(Component.text("% / ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) ++ .append(Component.text("%\n", PRIMARY)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Lowest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) ++ ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Median Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Highest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) ++ ++ .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) ++ .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) ++ .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(lowestRegionsBuilder.build()) ++ .build() ++ ); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ final String type; ++ if (args.length < 1) { ++ type = "server"; ++ } else { ++ type = args[0]; ++ } ++ ++ switch (type.toLowerCase(Locale.ROOT)) { ++ case "server": { ++ return executeServer(sender, commandLabel, args); ++ } ++ case "region": { ++ if (!(sender instanceof Entity)) { ++ sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); ++ return true; ++ } ++ return executeRegion(sender, commandLabel, args); ++ } ++ default: { ++ sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { ++ if (args.length == 0) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region")); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server")); ++ } ++ } else if (args.length == 1) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); ++ } ++ } ++ return new ArrayList<>(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d016294fc7eafbddf6d2a758e5803498dfa207b8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java +@@ -0,0 +1,121 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.util.HSVLike; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerPlayer; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Function; ++ ++public final class CommandUtil { ++ ++ public static List getSortedList(final Iterable iterable) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ ret.add(val); ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ if (val.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(val); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ final String transformed = transform.apply(val); ++ if (transformed != null) { ++ ret.add(transformed); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ final String string = transform.apply(val); ++ if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(string); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static TextColor getColourForTPS(final double tps) { ++ final double difference = Math.min(Math.abs(20.0 - tps), 20.0); ++ final double coordinate; ++ if (difference <= 2.0) { ++ // >= 18 tps ++ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); ++ } else if (difference <= 5.0) { ++ // >= 15 tps ++ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); ++ } else if (difference <= 10.0) { ++ // >= 10 tps ++ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); ++ } else { ++ // >= 0.0 tps ++ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getColourForMSPT(final double mspt) { ++ final double clamped = Math.min(Math.abs(mspt), 50.0); ++ final double coordinate; ++ if (clamped <= 15.0) { ++ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); ++ } else if (clamped <= 25.0) { ++ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); ++ } else if (clamped <= 35.0) { ++ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); ++ } else if (clamped <= 40.0) { ++ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); ++ } else { ++ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getUtilisationColourRegion(final double util) { ++ // TODO anything better? ++ // assume 20TPS ++ return getColourForMSPT(util * 50.0); ++ } ++ ++ public static ServerPlayer getPlayer(final String name) { ++ for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { ++ if (player.getGameProfile().getName().equalsIgnoreCase(name)) { ++ return player; ++ } ++ } ++ ++ return null; ++ } ++ ++ private CommandUtil() {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85d3965a67cfb59790c664baa7840b50436a5e28 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java +@@ -0,0 +1,424 @@ ++package io.papermc.paper.threadedregions.scheduler; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import io.papermc.paper.threadedregions.RegionizedData; ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import org.bukkit.Bukkit; ++import org.bukkit.World; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.plugin.IllegalPluginAccessException; ++import org.bukkit.plugin.Plugin; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Consumer; ++import java.util.logging.Level; ++ ++public final class FoliaRegionScheduler implements RegionScheduler { ++ ++ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { ++ return () -> { ++ try { ++ run.run(); ++ } catch (final Throwable throwable) { ++ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName() ++ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); ++ } ++ }; ++ } ++ ++ private static final RegionizedData SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); ++ ++ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) { ++ SCHEDULER_DATA.get().queueTask(task, delay); ++ } ++ ++ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) { ++ final World world = task.world; ++ if (world == null) { ++ // cancelled ++ return; ++ } ++ ++ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> { ++ scheduleInternalOnRegion(task, delay); ++ } ++ ); ++ } ++ ++ @Override ++ public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(run, "Runnable may not be null"); ++ ++ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run) ++ ); ++ } ++ ++ @Override ++ public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer task) { ++ return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1); ++ } ++ ++ @Override ++ public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final Consumer task, final long delayTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (delayTicks <= 0) { ++ throw new IllegalArgumentException("Delay ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { ++ scheduleInternalOnRegion(ret, delayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, delayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final Consumer task, final long initialDelayTicks, final long periodTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (initialDelayTicks <= 0) { ++ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); ++ } ++ if (periodTicks <= 0) { ++ throw new IllegalArgumentException("Period ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { ++ scheduleInternalOnRegion(ret, initialDelayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, initialDelayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ public void tick() { ++ SCHEDULER_DATA.get().tick(); ++ } ++ ++ private static final class Scheduler { ++ private static final RegionizedData.RegioniserCallback REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() { ++ @Override ++ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); ++ ++ for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> e = iterator.next(); ++ final long newTick = e.getLongKey() + fromTickOffset; ++ final List tasks = e.getValue(); ++ ++ sectionAdjusted.put(newTick, tasks); ++ } ++ ++ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); ++ } ++ } ++ ++ @Override ++ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final Scheduler into : dataSet) { ++ into.tickCount = from.tickCount; ++ } ++ ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Scheduler into = regionToData.get(sectionKey); ++ ++ into.tasksByDeadlineBySection.put(sectionKey, section); ++ } ++ } ++ }; ++ ++ private long tickCount = 0L; ++ // map of region section -> map of deadline -> list of tasks ++ private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); ++ ++ private void addTicket(final int sectionX, final int sectionZ) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; ++ final int shift = world.moonrise$getRegionChunkShift(); ++ final int chunkX = sectionX << shift; ++ final int chunkZ = sectionZ << shift; ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void removeTicket(final long sectionKey) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; ++ final int shift = world.moonrise$getRegionChunkShift(); ++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << shift; ++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << shift; ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void queueTask(final LocationScheduledTask task, final long delay) { ++ // note: must be on the thread that owns this scheduler ++ // note: delay > 0 ++ ++ final World world = task.world; ++ if (world == null) { ++ // cancelled ++ return; ++ } ++ ++ final int shift = ((CraftWorld)world).getHandle().moonrise$getRegionChunkShift(); ++ final int sectionX = task.chunkX >> shift; ++ final int sectionZ = task.chunkZ >> shift; ++ ++ final Long2ObjectOpenHashMap> section = ++ this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { ++ return new Long2ObjectOpenHashMap<>(); ++ } ++ ); ++ ++ if (section.isEmpty()) { ++ // need to keep the scheduler loaded for this location in order for tick() to be called... ++ this.addTicket(sectionX, sectionZ); ++ } ++ ++ section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(task); ++ } ++ ++ public void tick() { ++ ++this.tickCount; ++ ++ final List run = new ArrayList<>(); ++ ++ for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final List tasks = section.remove(this.tickCount); ++ ++ if (tasks == null) { ++ continue; ++ } ++ ++ run.addAll(tasks); ++ ++ if (section.isEmpty()) { ++ this.removeTicket(sectionKey); ++ sectionIterator.remove(); ++ } ++ } ++ ++ for (int i = 0, len = run.size(); i < len; ++i) { ++ run.get(i).run(); ++ } ++ } ++ } ++ ++ private static final class LocationScheduledTask implements ScheduledTask, Runnable { ++ ++ private static final int STATE_IDLE = 0; ++ private static final int STATE_EXECUTING = 1; ++ private static final int STATE_EXECUTING_CANCELLED = 2; ++ private static final int STATE_FINISHED = 3; ++ private static final int STATE_CANCELLED = 4; ++ ++ private final Plugin plugin; ++ private final int chunkX; ++ private final int chunkZ; ++ private final long repeatDelay; // in ticks ++ private World world; ++ private Consumer run; ++ ++ private volatile int state; ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); ++ ++ private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final long repeatDelay, final Consumer run) { ++ this.plugin = plugin; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ } ++ ++ private final int getStateVolatile() { ++ return (int)STATE_HANDLE.get(this); ++ } ++ ++ private final int compareAndExchangeStateVolatile(final int expect, final int update) { ++ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private final void setStateVolatile(final int value) { ++ STATE_HANDLE.setVolatile(this, value); ++ } ++ ++ @Override ++ public void run() { ++ if (!this.plugin.isEnabled()) { ++ // don't execute if the plugin is disabled ++ return; ++ } ++ ++ final boolean repeating = this.isRepeatingTask(); ++ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { ++ // cancelled ++ return; ++ } ++ ++ try { ++ this.run.accept(this); ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() ++ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); ++ } finally { ++ boolean reschedule = false; ++ if (!repeating) { ++ this.setStateVolatile(STATE_FINISHED); ++ } else if (!this.plugin.isEnabled()) { ++ this.setStateVolatile(STATE_CANCELLED); ++ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { ++ reschedule = true; ++ } // else: cancelled repeating task ++ ++ if (!reschedule) { ++ this.run = null; ++ this.world = null; ++ } else { ++ FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay); ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ for (int curr = this.getStateVolatile();;) { ++ switch (curr) { ++ case STATE_IDLE: { ++ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { ++ this.state = STATE_CANCELLED; ++ this.run = null; ++ this.world = null; ++ return CancelledState.CANCELLED_BY_CALLER; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + curr); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ final int state = this.getStateVolatile(); ++ switch (state) { ++ case STATE_IDLE: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + state); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java b/src/main/java/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..97cd0e767ed36eeb211ecdf125e8d2bfff19a15e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java +@@ -0,0 +1,79 @@ ++package io.papermc.paper.threadedregions.util; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.BitRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import java.util.concurrent.ThreadLocalRandom; ++ ++public final class SimpleThreadLocalRandomSource implements BitRandomSource { ++ ++ public static final SimpleThreadLocalRandomSource INSTANCE = new SimpleThreadLocalRandomSource(); ++ ++ private final PositionalRandomFactory positionalRandomFactory = new SimpleThreadLocalRandomSource.SimpleThreadLocalRandomPositionalRandomFactory(); ++ ++ private SimpleThreadLocalRandomSource() {} ++ ++ @Override ++ public int next(final int bits) { ++ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); ++ } ++ ++ @Override ++ public int nextInt() { ++ return ThreadLocalRandom.current().nextInt(); ++ } ++ ++ @Override ++ public int nextInt(final int bound) { ++ if (bound <= 0) { ++ throw new IllegalArgumentException(); ++ } ++ ++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ ++ final long value = (long)this.nextInt() & 0xFFFFFFFFL; ++ return (int)((value * (long)bound) >>> Integer.SIZE); ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ // no-op ++ } ++ ++ @Override ++ public double nextGaussian() { ++ return ThreadLocalRandom.current().nextGaussian(); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return this; ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ return this.positionalRandomFactory; ++ } ++ ++ private static final class SimpleThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { ++ ++ @Override ++ public RandomSource fromHashOf(final String seed) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource fromSeed(final long seed) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource at(final int x, final int y, final int z) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public void parityConfigString(final StringBuilder info) { ++ info.append("SimpleThreadLocalRandomPositionalRandomFactory{}"); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java b/src/main/java/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eda02661b1c09e5303d3912c2562bb1c4ccc04fe +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java +@@ -0,0 +1,73 @@ ++package io.papermc.paper.threadedregions.util; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.BitRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import java.util.concurrent.ThreadLocalRandom; ++ ++public final class ThreadLocalRandomSource implements BitRandomSource { ++ ++ public static final ThreadLocalRandomSource INSTANCE = new ThreadLocalRandomSource(); ++ ++ private final PositionalRandomFactory positionalRandomFactory = new ThreadLocalRandomPositionalRandomFactory(); ++ ++ private ThreadLocalRandomSource() {} ++ ++ @Override ++ public int next(final int bits) { ++ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); ++ } ++ ++ @Override ++ public int nextInt() { ++ return ThreadLocalRandom.current().nextInt(); ++ } ++ ++ @Override ++ public int nextInt(final int bound) { ++ return ThreadLocalRandom.current().nextInt(bound); ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ // no-op ++ } ++ ++ @Override ++ public double nextGaussian() { ++ return ThreadLocalRandom.current().nextGaussian(); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return this; ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ return this.positionalRandomFactory; ++ } ++ ++ private static final class ThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { ++ ++ @Override ++ public RandomSource fromHashOf(final String seed) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource fromSeed(final long seed) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource at(final int x, final int y, final int z) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public void parityConfigString(final StringBuilder info) { ++ info.append("ThreadLocalRandomPositionalRandomFactory{}"); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index a4ac34ebb58a404f4fca7e763e61d4ab05ee3af4..4dcec640f5870d713bd3b98389a45dbef8a4ea8a 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -94,6 +94,7 @@ public final class MCUtil { + */ + public static void ensureMain(String reason, Runnable run) { + if (!isMainThread()) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + if (reason != null) { + MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); + } +@@ -148,6 +149,30 @@ public final class MCUtil { + return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); + } + ++ // Folia start - TODO MERGE INTO MCUTIL ++ /** ++ * Converts a NMS World/Vector to Bukkit Location ++ * @param world ++ * @param pos ++ * @return ++ */ ++ public static Location toLocation(Level world, Vec3 pos) { ++ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z()); ++ } ++ ++ /** ++ * Converts a NMS World/Vector to Bukkit Location ++ * @param world ++ * @param pos ++ * @param yaw ++ * @param pitch ++ * @return ++ */ ++ public static Location toLocation(Level world, Vec3 pos, float yaw, float pitch) { ++ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); ++ } ++ // Folia end - TODO MERGE INTO MCUTIL ++ + public static BlockPos toBlockPosition(Location loc) { + return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + } +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index 13bd145b1e8006a53c22f5dc0c78f29b540c7663..6d87797523337725141f271087f80065ed67347e 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -69,7 +69,7 @@ public class CommandSourceStack implements ExecutionCommandSource { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);})); // Folia - region threading + } + + protected CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity, boolean silent, CommandResultCallback resultStorer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext signedArguments, TaskChainer messageChainTaskQueue) { +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index 517cb238ec280aadd1fc54bcb675ed386e798eaf..7f16e3040f4686ffe6338290db33d2733bd6659f 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -163,13 +163,13 @@ public class Commands { + AdvancementCommands.register(this.dispatcher); + AttributeCommand.register(this.dispatcher, commandRegistryAccess); + ExecuteCommand.register(this.dispatcher, commandRegistryAccess); +- BossBarCommands.register(this.dispatcher, commandRegistryAccess); ++ //BossBarCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO + ClearInventoryCommands.register(this.dispatcher, commandRegistryAccess); +- CloneCommands.register(this.dispatcher, commandRegistryAccess); ++ //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO + DamageCommand.register(this.dispatcher, commandRegistryAccess); +- DataCommands.register(this.dispatcher); +- DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //DataCommands.register(this.dispatcher); // Folia - region threading - TODO ++ //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO ++ //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, commandRegistryAccess); +@@ -179,47 +179,47 @@ public class Commands { + FillCommand.register(this.dispatcher, commandRegistryAccess); + FillBiomeCommand.register(this.dispatcher, commandRegistryAccess); + ForceLoadCommand.register(this.dispatcher); +- FunctionCommand.register(this.dispatcher); ++ //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO + GameModeCommand.register(this.dispatcher); + GameRuleCommand.register(this.dispatcher, commandRegistryAccess); + GiveCommand.register(this.dispatcher, commandRegistryAccess); + HelpCommand.register(this.dispatcher); +- ItemCommands.register(this.dispatcher, commandRegistryAccess); ++ //ItemCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later + KickCommand.register(this.dispatcher); + KillCommand.register(this.dispatcher); + ListPlayersCommand.register(this.dispatcher); + LocateCommand.register(this.dispatcher, commandRegistryAccess); +- LootCommand.register(this.dispatcher, commandRegistryAccess); ++ //LootCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later + MsgCommand.register(this.dispatcher); + ParticleCommand.register(this.dispatcher, commandRegistryAccess); + PlaceCommand.register(this.dispatcher); + PlaySoundCommand.register(this.dispatcher); + RandomCommand.register(this.dispatcher); +- ReloadCommand.register(this.dispatcher); ++ //ReloadCommand.register(this.dispatcher); // Folia - region threading + RecipeCommand.register(this.dispatcher); +- ReturnCommand.register(this.dispatcher); +- RideCommand.register(this.dispatcher); +- RotateCommand.register(this.dispatcher); ++ //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later + SayCommand.register(this.dispatcher); +- ScheduleCommand.register(this.dispatcher); +- ScoreboardCommand.register(this.dispatcher, commandRegistryAccess); ++ //ScheduleCommand.register(this.dispatcher); // Folia - region threading ++ //ScoreboardCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later + SeedCommand.register(this.dispatcher, environment != Commands.CommandSelection.INTEGRATED); + SetBlockCommand.register(this.dispatcher, commandRegistryAccess); + SetSpawnCommand.register(this.dispatcher); + SetWorldSpawnCommand.register(this.dispatcher); +- SpectateCommand.register(this.dispatcher); +- SpreadPlayersCommand.register(this.dispatcher); ++ //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later + StopSoundCommand.register(this.dispatcher); + SummonCommand.register(this.dispatcher, commandRegistryAccess); +- TagCommand.register(this.dispatcher); +- TeamCommand.register(this.dispatcher, commandRegistryAccess); +- TeamMsgCommand.register(this.dispatcher); ++ //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //TeamCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later ++ //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later + TeleportCommand.register(this.dispatcher); + TellRawCommand.register(this.dispatcher, commandRegistryAccess); +- TickCommand.register(this.dispatcher); ++ //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later + TimeCommand.register(this.dispatcher); + TitleCommand.register(this.dispatcher, commandRegistryAccess); +- TriggerCommand.register(this.dispatcher); ++ //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later + WeatherCommand.register(this.dispatcher); + WorldBorderCommand.register(this.dispatcher); + if (JvmProfiler.INSTANCE.isAvailable()) { +@@ -247,8 +247,8 @@ public class Commands { + OpCommand.register(this.dispatcher); + PardonCommand.register(this.dispatcher); + PardonIpCommand.register(this.dispatcher); +- PerfCommand.register(this.dispatcher); +- SaveAllCommand.register(this.dispatcher); ++ //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later + SaveOffCommand.register(this.dispatcher); + SaveOnCommand.register(this.dispatcher); + SetPlayerIdleTimeoutCommand.register(this.dispatcher); +@@ -507,9 +507,12 @@ public class Commands { + } + // Paper start - Perf: Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API +- net.minecraft.server.MinecraftServer.getServer().execute(() -> { +- runSync(player, bukkit, rootcommandnode); +- }); ++ // Folia start - region threading ++ // ignore if retired ++ player.getBukkitEntity().taskScheduler.schedule((updatedPlayer) -> { ++ runSync((ServerPlayer)updatedPlayer, bukkit, rootcommandnode); ++ }, null, 1L); ++ // Folia end - region threading + } + + private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { +diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +index ddb264443f2e38b6348226016f9139727c588898..b8c3d6aa9356beeb4bfb431e8d2dd7367969dd8d 100644 +--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +@@ -54,7 +54,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +index 8aae1d113e84dfad9f2b6f0bcd203ca6c68bc5ce..ca98699f4225192bb3364397402b07001b1402af 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +@@ -87,7 +87,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index c9d7ac819ce26f5301df7df56edce59b7ef377e0..fd5f2864c670c1580e07d67c47bc61d8b354a687 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -111,7 +111,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -170,7 +170,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -225,7 +225,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -280,7 +280,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -354,7 +354,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -419,7 +419,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -457,7 +457,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -519,7 +519,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -537,7 +537,8 @@ public interface DispenseItemBehavior { + } + } + +- worldserver.captureTreeGeneration = true; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = worldserver.getCurrentWorldData(); // Folia - region threading ++ worldData.captureTreeGeneration = true; // Folia - region threading + // CraftBukkit end + + if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) { +@@ -546,13 +547,13 @@ public interface DispenseItemBehavior { + worldserver.levelEvent(1505, blockposition, 15); + } + // CraftBukkit start +- worldserver.captureTreeGeneration = false; +- if (worldserver.capturedBlockStates.size() > 0) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ worldData.captureTreeGeneration = false; // Folia - region threading ++ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ SaplingBlock.treeTypeRT.set(null); // Folia - region threading + Location location = CraftLocation.toBukkit(blockposition, worldserver.getWorld()); +- List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values()); +- worldserver.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + StructureGrowEvent structureEvent = null; + if (treeType != null) { + structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks); +@@ -588,7 +589,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -631,7 +632,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -680,7 +681,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -742,7 +743,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -824,7 +825,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +index bf8c511739265c6a9cd277752e844481598f8966..c369c0cad19ade00a1e62e834568f1049fb021cd 100644 +--- a/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +@@ -50,7 +50,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +index 3588896b7413be73ade6b3f8fd111d02c48ec550..add1e68b3be50ad5f2894fa40182f5448dcf2537 100644 +--- a/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +@@ -74,7 +74,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec3d1.x, vec3d1.y, vec3d1.z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +index 54c72cf472e06e214eb61bd8615a0bb27690c807..f8cb78e6347eb4cb0c5942ae8c75728ed1389970 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +@@ -43,7 +43,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index 65ed3d77a51b8299517e0c165403b0c5ac413475..bb1180f8739bb211c220950d4612eb8e59ee4afe 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -41,7 +41,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +index 8f9fde5489c0e1d0a91203536caddec5a9c96f6c..d43489fa1f69f025d44b881205afba39bdebba1c 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + pointer.level().getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 3c866432c8a938c677a315612f3e159bda67a2a2..30e3d0d7b0ca8cfa30efb60eefa73d94c65c4690 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -93,7 +93,7 @@ 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(); // Paper ++ private final Queue pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Paper // Folia - region threading - connection fixes + public Channel channel; + public SocketAddress address; + // Spigot Start +@@ -108,7 +108,7 @@ public class Connection extends SimpleChannelInboundHandler> { + @Nullable + private DisconnectionDetails disconnectionDetails; + private boolean encrypted; +- private boolean disconnectionHandled; ++ private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage + private int receivedPackets; + private int sentPackets; + private float averageReceivedPackets; +@@ -163,6 +163,32 @@ public class Connection extends SimpleChannelInboundHandler> { + this.receiving = side; + } + ++ // Folia start - region threading ++ private volatile boolean becomeActive; ++ ++ public boolean becomeActive() { ++ return this.becomeActive; ++ } ++ ++ private static record DisconnectReq(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} ++ ++ private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = ++ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); ++ ++ /** ++ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the ++ * same thread that could disconnect. ++ */ ++ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { ++ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); ++ // We can't halt packet processing here because a plugin could cancel a kick request. ++ } ++ ++ public final boolean isPlayerConnected() { ++ return this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl; ++ } ++ // Folia end - region threading ++ + public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception { + super.channelActive(channelhandlercontext); + this.channel = channelhandlercontext.channel(); +@@ -173,7 +199,7 @@ public class Connection extends SimpleChannelInboundHandler> { + if (this.delayedDisconnect != null) { + this.disconnect(this.delayedDisconnect); + } +- ++ this.becomeActive = true; // Folia - region threading + } + + public void channelInactive(ChannelHandlerContext channelhandlercontext) { +@@ -451,7 +477,7 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + packet.onPacketDispatch(this.getPlayer()); +- if (connected && (InnerUtil.canSendImmediate(this, packet) ++ if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes + || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() + && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { + this.sendPacket(packet, callbacks, flush); +@@ -480,11 +506,12 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + public void runOnceConnected(Consumer task) { +- if (this.isConnected()) { ++ if (false && this.isConnected()) { // Folia - region threading - connection fixes + this.flushQueue(); + task.accept(this); + } else { + this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network ++ this.flushQueue(); // Folia - region threading - connection fixes + } + + } +@@ -543,10 +570,11 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + public void flushChannel() { +- if (this.isConnected()) { ++ if (false && this.isConnected()) { // Folia - region threading - connection fixes + this.flush(); + } else { + this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network ++ this.flushQueue(); // Folia - region threading - connection fixes + } + + } +@@ -564,53 +592,61 @@ public class Connection extends SimpleChannelInboundHandler> { + + // 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) { +- return this.processQueue(); +- } +- } +- return false; ++ return this.processQueue(); // Folia - region threading - connection fixes + } + ++ // Folia start - region threading - connection fixes ++ // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent ++ // into the queue ++ private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); ++ ++ private static boolean canWrite(WrappedConsumer queued) { ++ return queued != null && (!(queued instanceof PacketSendAction packet) || packet.packet.isReady()); ++ } ++ ++ private boolean canWritePackets() { ++ return canWrite(this.pendingActions.peek()); ++ } ++ // Folia end - region threading - connection fixes ++ + private boolean processQueue() { +- if (this.pendingActions.isEmpty()) { ++ // Folia start - region threading - connection fixes ++ if (!this.isConnected()) { + return true; + } + +- // 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; +- } ++ while (this.canWritePackets()) { ++ final boolean set = this.flushingQueue.getAndSet(true); ++ try { ++ if (set) { ++ // we didn't acquire the lock, break ++ return false; ++ } + +- if (queued.isConsumed()) { +- continue; +- } ++ ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue queue = ++ (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.pendingActions; ++ WrappedConsumer holder; ++ for (;;) { ++ // synchronise so that queue clears appear atomic ++ synchronized (queue) { ++ holder = queue.pollIf(Connection::canWrite); ++ } ++ if (holder == null) { ++ break; ++ } + +- if (queued instanceof PacketSendAction packetSendAction) { +- final Packet packet = packetSendAction.packet; +- if (!packet.isReady()) { +- return false; ++ holder.accept(this); + } +- } + +- iterator.remove(); +- if (queued.tryMarkConsumed()) { +- queued.accept(this); ++ } finally { ++ if (!set) { ++ this.flushingQueue.set(false); ++ } + } + } ++ + return true; ++ // Folia end - region threading - connection fixes + } + // Paper end - Optimize network + +@@ -619,19 +655,39 @@ public class Connection extends SimpleChannelInboundHandler> { + private static int currTick; // Paper - Buffer joins to world + public void tick() { + this.flushQueue(); +- // Paper start - Buffer joins to world +- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { +- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; +- Connection.joinAttemptsThisTick = 0; ++ // Folia - this is broken ++ // Folia start - region threading ++ // handle disconnect requests, but only after flushQueue() ++ DisconnectReq disconnectReq; ++ while ((disconnectReq = this.disconnectReqs.poll()) != null) { ++ PacketListener packetlistener = this.packetListener; ++ ++ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ loginPacketListener.disconnect(disconnectReq.disconnectReason); ++ // this doesn't fail, so abort any further attempts ++ return; ++ } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { ++ commonPacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); ++ // may be cancelled by a plugin, if not cancelled then any further calls do nothing ++ continue; ++ } else { ++ // no idea what packet to send ++ this.disconnect(disconnectReq.disconnectReason); ++ this.setReadOnly(); ++ return; ++ } + } +- // Paper end - Buffer joins to world ++ if (!this.isConnected()) { ++ // disconnected from above ++ this.handleDisconnection(); ++ return; ++ } ++ // Folia end - region threading + PacketListener packetlistener = this.packetListener; + + if (packetlistener instanceof TickablePacketListener tickablepacketlistener) { + // Paper start - Buffer joins to world +- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) +- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING +- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { ++ if (true) { // Folia - region threading + // Paper start - detailed watchdog information + net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); + try { +@@ -642,7 +698,7 @@ public class Connection extends SimpleChannelInboundHandler> { + } // Paper end - Buffer joins to world + } + +- if (!this.isConnected() && !this.disconnectionHandled) { ++ if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs + this.handleDisconnection(); + } + +@@ -692,6 +748,7 @@ public class Connection extends SimpleChannelInboundHandler> { + this.channel.close(); // We can't wait as this may be called from an event loop. + this.disconnectionDetails = disconnectionInfo; + } ++ this.becomeActive = true; // Folia - region threading + + } + +@@ -895,10 +952,10 @@ public class Connection extends SimpleChannelInboundHandler> { + + public void handleDisconnection() { + if (this.channel != null && !this.channel.isOpen()) { +- if (this.disconnectionHandled) { ++ if (this.disconnectionHandled.getAndSet(true)) { // Folia - region threading - may be called concurrently during configuration stage + // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message + } else { +- this.disconnectionHandled = true; ++ // Folia - region threading - may be called concurrently during configuration stage - set above + PacketListener packetlistener = this.getPacketListener(); + PacketListener packetlistener1 = packetlistener != null ? packetlistener : this.disconnectListener; + +@@ -930,6 +987,22 @@ public class Connection extends SimpleChannelInboundHandler> { + } + } + // Paper end - Add PlayerConnectionCloseEvent ++ // Folia start - region threading ++ if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ commonPacketListener.getOwner().getName(), ++ commonPacketListener.getOwner().getId(), this ++ ); ++ } else if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) { ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ loginPacketListener.authenticatedProfile.getName(), ++ loginPacketListener.authenticatedProfile.getId(), this ++ ); ++ } ++ } ++ // Folia end - region threading ++ // Paper end + + } + } +@@ -950,15 +1023,25 @@ public class Connection extends SimpleChannelInboundHandler> { + // 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); ++ // Folia start - region threading - connection fixes ++ java.util.List queuedPackets = new java.util.ArrayList<>(); ++ // synchronise so that flushQueue does not poll values while the queue is being cleared ++ synchronized (this.pendingActions) { ++ Connection.WrappedConsumer consumer; ++ while ((consumer = this.pendingActions.poll()) != null) { ++ if (consumer instanceof Connection.PacketSendAction packetHolder) { ++ queuedPackets.add(packetHolder); + } + } + } +- this.pendingActions.clear(); ++ ++ for (Connection.PacketSendAction queuedPacket : queuedPackets) { ++ Packet packet = queuedPacket.packet; ++ if (packet.hasFinishListener()) { ++ packet.onPacketDispatchFinish(player, null); ++ } ++ } ++ // Folia end - region threading - connection fixes + } + + private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index 1f7f68aad97ee73763c042837f239bdc7167db55..d6eb8f495688a1b65a4c419aa3ee655cd8eb322a 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -46,7 +46,7 @@ public class PacketUtils { + + public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + if (!engine.isSameThread()) { +- engine.executeIfPossible(() -> { ++ Runnable run = () -> { // Folia - region threading + packetProcessing.push(listener); // Paper - detailed watchdog information + try { // Paper - detailed watchdog information + if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players +@@ -74,7 +74,23 @@ public class PacketUtils { + } + // Paper end - detailed watchdog information + +- }); ++ }; // Folia start - region threading ++ // ignore retired state, if removed then we don't want the packet to be handled ++ if (listener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { ++ gamePacketListener.player.getBukkitEntity().taskScheduler.schedule( ++ (net.minecraft.server.level.ServerPlayer player) -> { ++ run.run(); ++ }, ++ null, 1L ++ ); ++ } else if (listener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); ++ } else if (listener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); ++ } else { ++ throw new UnsupportedOperationException("Unknown listener: " + listener); ++ } ++ // Folia end - region threading + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; + } + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index ae4ebf509837e8d44255781c61d02873f8b74be8..25b4b0d531f0698338ffeac686013a4631d60c00 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -216,7 +216,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + // Paper - don't store the vanilla dispatcher +@@ -334,6 +333,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop CompletableFuture submit(java.util.function.Supplier task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public CompletableFuture submit(Runnable task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public void schedule(TickTask task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.schedule(task); ++ } ++ ++ @Override ++ public void executeBlocking(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeBlocking(runnable); ++ } ++ ++ @Override ++ public void execute(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.execute(runnable); ++ } ++ // Folia end - regionised ticking ++ + public static S spin(Function serverFactory) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system + AtomicReference atomicreference = new AtomicReference(); +@@ -368,46 +411,30 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { +- this.lastMidTickExecuteFailure = currTime; ++ worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading + } + + // note: negative values reduce the time +@@ -420,7 +447,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4; ++ world.randomSpawnSelection = new ChunkPos(world.getChunkSource().randomState().sampler().findSpawnPosition()); ++ for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { ++ for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { ++ ChunkPos pos = new ChunkPos(currX, currZ); ++ world.chunkSource.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos ++ ); ++ } ++ } ++ // Folia end - region threading + + // Paper - Put world into worldlist before initing the world; move up + this.getPlayerList().addWorldborderListener(world); +@@ -749,6 +790,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 ? Mth.square(ChunkProgressListener.calculateDiameter(i)) : 0; + +- while (chunkproviderserver.getTickingGenerated() < j) { +- // CraftBukkit start +- // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS; +- this.executeModerately(); +- } ++ // Folia - region threading + + // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS; +- this.executeModerately(); ++ //this.executeModerately(); // Folia - region threading + // Iterator iterator = this.levels.values().iterator(); + + if (true) { +@@ -938,7 +977,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- return false; +- } : this::haveTime); ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + // Paper start - rewrite chunk system + final Throwable crash = this.chunkSystemCrash; + if (crash != null) { +@@ -1531,21 +1623,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; +- } +- // Paper end +- return new TickTask(this.tickCount, runnable); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + protected boolean shouldRun(TickTask ticktask) { +- return ticktask.getTick() + 3 < this.tickCount || this.haveTime(); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + @Override + public boolean pollTask() { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + boolean flag = this.pollTaskInternal(); + + this.mayHaveDelayedTasks = flag; +@@ -1553,6 +1640,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { ++ if (false && j > 0) { // Folia - region threading - this is complicated to implement, and even if done correctly is messy + if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping + ++this.emptyTicks; + } else { +@@ -1652,24 +1744,59 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop true, false); + } + // Paper end - avoid issues with certain tasks not processing during sleep +- this.server.spark.executeMainThreadTasks(); // Paper - spark ++ // Folia - region threading + this.tickConnection(); + this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark + return; + } + } + ++ // Folia start - region threading ++ if (region != null) { ++ region.world.getCurrentWorldData().updateTickData(); ++ if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) { ++ synchronized (region.world.checkInitialised) { ++ if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) { ++ LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked..."); ++ this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world ++ region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED); ++ LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'"); ++ } // else: must be checked ++ } ++ } ++ } ++ BooleanSupplier shouldKeepTicking = () -> { ++ return scheduledEnd - System.nanoTime() > targetBuffer; ++ }; ++ // Folia end - region threading ++ + this.server.spark.tickStart(); // Paper - spark +- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events +- ++this.tickCount; +- this.tickRateManager.tick(); +- this.tickChildren(shouldKeepTicking); +- if (i - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { ++ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper - Server Tick Events // Folia - region threading ++ // Folia start - region threading ++ if (region != null) { ++ region.getTaskQueueData().drainTasks(); ++ ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)Bukkit.getRegionScheduler()).tick(); ++ // now run all the entity schedulers ++ // TODO there has got to be a more efficient variant of this crap ++ for (Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { ++ continue; ++ } ++ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); ++ if (bukkit != null) { ++ bukkit.taskScheduler.executeTick(); ++ } ++ } ++ } ++ // Folia end - region threading ++ if (region == null) this.tickRateManager.tick(); // Folia - region threading ++ this.tickChildren(shouldKeepTicking, region); // Folia - region threading ++ if (region == null && i - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading + this.lastServerStatus = i; + this.status = this.buildServerStatus(); + } + +- --this.ticksUntilAutosave; ++ // Folia - region threading + // Paper start - Incremental chunk and player saving + final ProfilerFiller profiler = Profiler.get(); + int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; +@@ -1677,15 +1804,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.tickCount % autosavePeriod == 0; ++ final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading + try { + this.isSaving = true; + if (playerSaveInterval > 0) { + this.playerList.saveAll(playerSaveInterval); + } +- for (final ServerLevel level : this.getAllLevels()) { ++ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading + if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { +- level.saveIncrementally(fullSave); ++ level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat + } + } + } finally { +@@ -1696,33 +1823,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list = this.playerList.getPlayers(); ++ List list = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading + int i = this.getMaxPlayers(); + + if (this.hidesOnlinePlayers()) { +@@ -1796,48 +1899,38 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ if (region == null) this.getPlayerList().getPlayers().forEach((entityplayer) -> { // Folia - region threading + entityplayer.connection.suspendFlushing(); + }); +- this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit ++ // Folia - region threading + // Paper start - Folia scheduler API +- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick(); +- getAllLevels().forEach(level -> { +- for (final Entity entity : level.getEntities().getAll()) { +- if (entity.isRemoved()) { +- continue; +- } +- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); +- if (bukkit != null) { +- bukkit.taskScheduler.executeTick(); +- } +- } +- }); ++ // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion + // Paper end - Folia scheduler API +- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper ++ // Folia - region threading - moved to global tick + gameprofilerfiller.push("commandFunctions"); +- this.getFunctions().tick(); ++ if (region == null) this.getFunctions().tick(); // Folia - region threading - TODO Purge functions + gameprofilerfiller.popPush("levels"); + //Iterator iterator = this.getAllLevels().iterator(); // Paper - Throw exception on world create while being ticked; moved down + + // CraftBukkit start + // Run tasks that are waiting on processing +- while (!this.processQueue.isEmpty()) { ++ if (region == null) while (!this.processQueue.isEmpty()) { // Folia - region threading + this.processQueue.remove().run(); + } + + // Send time updates to everyone, it will get the right time from the world the player is in. + // Paper start - Perf: Optimize time updates +- for (final ServerLevel level : this.getAllLevels()) { ++ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading + final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT); + final long dayTime = level.getDayTime(); + long worldTime = level.getGameTime(); + final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight); +- for (Player entityhuman : level.players()) { +- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) { ++ for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading ++ if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading + continue; + } + ServerPlayer entityplayer = (ServerPlayer) entityhuman; +@@ -1849,14 +1942,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent +- worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent +- net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers +- worldserver.updateLagCompensationTick(); // Paper - lag compensation ++ // Folia - region threading + + gameprofilerfiller.push(() -> { + String s = String.valueOf(worldserver); +@@ -1874,7 +1964,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); ++ }); ++ return; ++ } ++ // Folia end - region threading + this.lastServerStatus = 0L; + } + +@@ -2321,6 +2421,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; ++ return false; // Folia - region threading + } + + public void addPluginAllowingSleep(final String pluginName, final boolean value) { +- if (!value) { +- this.pluginsBlockingSleep.add(pluginName); +- } else { +- this.pluginsBlockingSleep.remove(pluginName); +- } ++ // Folia - region threading + } + + private void removeDisabledPluginsBlockingSleep() { +- if (this.pluginsBlockingSleep.isEmpty()) { +- return; +- } +- this.pluginsBlockingSleep.removeIf(plugin -> ( +- !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) +- )); ++ // Folia - region threading + } + // Paper end - API to check if the server is sleeping + } +diff --git a/src/main/java/net/minecraft/server/commands/AdvancementCommands.java b/src/main/java/net/minecraft/server/commands/AdvancementCommands.java +index 9113e1ae4d63941d8c30f366b15cf649f063e1a6..6e0b85bf57670c743e0e8bb0a996614a9108f8eb 100644 +--- a/src/main/java/net/minecraft/server/commands/AdvancementCommands.java ++++ b/src/main/java/net/minecraft/server/commands/AdvancementCommands.java +@@ -256,7 +256,12 @@ public class AdvancementCommands { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += operation.perform(serverPlayer, selection); ++ // Folia start - region threading ++ i += 1; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ operation.perform(player, selection); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +@@ -320,9 +325,12 @@ public class AdvancementCommands { + throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterion); + } else { + for (ServerPlayer serverPlayer : targets) { +- if (operation.performCriterion(serverPlayer, advancement, criterion)) { +- i++; +- } ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ operation.performCriterion(player, advancement, criterion); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/AttributeCommand.java b/src/main/java/net/minecraft/server/commands/AttributeCommand.java +index 3ca51820d0fcf3f5c9f86b0d2fd8b2469bfb9795..af7a7fcd6377db1e61613977c7bb0ba1faa98d9c 100644 +--- a/src/main/java/net/minecraft/server/commands/AttributeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/AttributeCommand.java +@@ -262,37 +262,76 @@ public class AttributeCommand { + } + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int getAttributeValue(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + double d = livingEntity.getAttributeValue(attribute); +- source.sendSuccess(() -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), target.getName(), d), false); +- return (int)(d * multiplier); ++ source.sendSuccess(() -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); // Folia - region threading ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int getAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + double d = livingEntity.getAttributeBaseValue(attribute); + source.sendSuccess( +- () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), target.getName(), d), false ++ () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false // Folia - region threading + ); +- return (int)(d * multiplier); ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int getAttributeModifier(CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + AttributeMap attributeMap = livingEntity.getAttributes(); + if (!attributeMap.hasModifier(attribute, id)) { +- throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } else { + double d = attributeMap.getModifierValue(attribute, id); + source.sendSuccess( + () -> Component.translatable( +- "commands.attribute.modifier.value.get.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName(), d ++ "commands.attribute.modifier.value.get.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName(), d // Folia - region threading + ), + false + ); +- return (int)(d * multiplier); ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static Stream getAttributeModifiers(Entity target, Holder attribute) throws CommandSyntaxException { +@@ -301,11 +340,22 @@ public class AttributeCommand { + } + + private static int setAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double value) throws CommandSyntaxException { +- getAttributeInstance(target, attribute).setBaseValue(value); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ getAttributeInstance(nmsEntity, attribute).setBaseValue(value); // Folia - region threading + source.sendSuccess( +- () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), target.getName(), value), false ++ () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false // Folia - region threading + ); +- return 1; ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int resetAttributeBase(CommandSourceStack source, Entity target, Holder attribute) throws CommandSyntaxException { +@@ -324,35 +374,57 @@ public class AttributeCommand { + private static int addModifier( + CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id, double value, AttributeModifier.Operation operation + ) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(target, attribute); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading + AttributeModifier attributeModifier = new AttributeModifier(id, value, operation); + if (attributeInstance.hasModifier(id)) { +- throw ERROR_MODIFIER_ALREADY_PRESENT.create(target.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } else { + attributeInstance.addPermanentModifier(attributeModifier); + source.sendSuccess( + () -> Component.translatable( +- "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName() ++ "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading + ), + false + ); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int removeModifier(CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(target, attribute); ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading + if (attributeInstance.removeModifier(id)) { + source.sendSuccess( + () -> Component.translatable( +- "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName() ++ "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading + ), + false + ); +- return 1; ++ return; // Folia - region threading + } else { +- throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static Component getAttributeDescription(Holder attribute) { +diff --git a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +index 4e6171ca870649114d4c7460baad2982173da09e..f8f0d33c663e6b9adac9f7e3eb21b5a6ff860c5d 100644 +--- a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java ++++ b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +@@ -65,9 +65,14 @@ public class ClearInventoryCommands { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); +- serverPlayer.containerMenu.broadcastChanges(); +- serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.getInventory().clearOrCountMatchingItems(item, maxCount, player.inventoryMenu.getCraftSlots()); ++ player.containerMenu.broadcastChanges(); ++ player.inventoryMenu.slotsChanged(player.getInventory()); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/DamageCommand.java b/src/main/java/net/minecraft/server/commands/DamageCommand.java +index 4184f5e8785b54694108cbbb231cab503803109d..d8a64ca978545c0ff9957fa828b07912ed03f377 100644 +--- a/src/main/java/net/minecraft/server/commands/DamageCommand.java ++++ b/src/main/java/net/minecraft/server/commands/DamageCommand.java +@@ -104,12 +104,29 @@ public class DamageCommand { + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageSource) throws CommandSyntaxException { +- if (target.hurtServer(source.getLevel(), damageSource, amount)) { +- source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, target.getDisplayName()), true); +- return 1; ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ if (nmsEntity.hurtServer(source.getLevel(), damageSource, amount)) { // Folia - region threading ++ source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, nmsEntity.getDisplayName()), true); // Folia - region threading ++ return; // Folia - region threading + } else { + throw ERROR_INVULNERABLE.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +index a046a0b1519806ff3d987e6402f152b60a3a6f4c..60649ced046fa126ef59b7a2dc3e6b5eeaf42bc4 100644 +--- a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java ++++ b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +@@ -28,12 +28,14 @@ public class DefaultGameModeCommands { + GameType gameType = minecraftServer.getForcedGameType(); + if (gameType != null) { + for (ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + // Paper start - Expand PlayerGameModeChangeEvent +- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); ++ org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading + if (event != null && event.isCancelled()) { + source.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); + } + // Paper end - Expand PlayerGameModeChangeEvent ++ }, null, 1L); // Folia - region threading + i++; + } + } +diff --git a/src/main/java/net/minecraft/server/commands/EffectCommands.java b/src/main/java/net/minecraft/server/commands/EffectCommands.java +index ed6336065a0061af095d3395b927b8976443cb68..5a6f53cfaded2df14a82ee7639cdd0bb4728012e 100644 +--- a/src/main/java/net/minecraft/server/commands/EffectCommands.java ++++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java +@@ -84,7 +84,15 @@ public class EffectCommands { + if (entity instanceof LivingEntity) { + MobEffectInstance mobeffect = new MobEffectInstance(statusEffect, k, amplifier, false, showParticles); + +- if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).addEffect(mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading ++ if (true) { // CraftBukkit // Folia - region threading + ++j; + } + } +@@ -114,8 +122,16 @@ public class EffectCommands { + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading + ++i; ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading + } + } + +@@ -144,8 +160,16 @@ public class EffectCommands { + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(statusEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading + ++i; ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).removeEffect(statusEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index cf0a5943f457c532958f40b4989fa18f967abae6..10422cbac32c7dfbd65cda3157f30b4d4c5ce9a2 100644 +--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java ++++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +@@ -68,51 +68,79 @@ public class EnchantCommand { + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment2 = enchantment.value(); + if (level > enchantment2.getMaxLevel()) { + throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); + } else { +- int i = 0; ++ final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading ++ final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading ++ final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading + + for (Entity entity : targets) { + if (entity instanceof LivingEntity) { +- LivingEntity livingEntity = (LivingEntity)entity; +- ItemStack itemStack = livingEntity.getMainHandItem(); +- if (!itemStack.isEmpty()) { +- if (enchantment2.canEnchant(itemStack) +- && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment)) { +- itemStack.enchant(enchantment, level); +- i++; +- } else if (targets.size() == 1) { +- throw ERROR_INCOMPATIBLE.create(itemStack.getHoverName().getString()); ++ ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = (LivingEntity)nmsEntity; ++ ItemStack itemStack = livingEntity.getMainHandItem(); ++ if (!itemStack.isEmpty()) { ++ if (enchantment2.canEnchant(itemStack) ++ && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment)) { ++ itemStack.enchant(enchantment, level); ++ possibleSingleDisplayName.set(livingEntity.getDisplayName()); ++ changed.incrementAndGet(); ++ } else if (targets.size() == 1) { ++ throw ERROR_INCOMPATIBLE.create(itemStack.getHoverName().getString()); ++ } ++ } else if (targets.size() == 1) { ++ throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); ++ } ++ } catch (final CommandSyntaxException exception) { ++ sendMessage(source, exception); ++ return; // don't send feedback twice + } +- } else if (targets.size() == 1) { +- throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); +- } ++ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); ++ }, ignored -> sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed), 1L); + } else if (targets.size() == 1) { + throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); ++ } else { ++ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); ++ // Folia end - region threading + } + } ++ return targets.size(); // Folia - region threading ++ } ++ } + ++ // Folia start - region threading ++ private static void sendFeedback(final CommandSourceStack source, final Holder enchantment, final int level, final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) { ++ if (count.decrementAndGet() == 0) { ++ final int i = changed.get(); + if (i == 0) { +- throw ERROR_NOTHING_HAPPENED.create(); ++ sendMessage(source, ERROR_NOTHING_HAPPENED.create()); + } else { +- if (targets.size() == 1) { ++ if (i == 1) { + source.sendSuccess( + () -> Component.translatable( +- "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), targets.iterator().next().getDisplayName() ++ "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), possibleSingleDisplayName.get() + ), + true + ); + } else { + source.sendSuccess( +- () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), targets.size()), true ++ () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), i), true + ); + } +- +- return i; + } + } + } ++ // Folia end - region threading + } +diff --git a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +index b4578e25286a245bba84543fac50c415dc20ba36..89cc58ba70930822f7a678577b8b7c7077a35791 100644 +--- a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java ++++ b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +@@ -131,14 +131,18 @@ public class ExperienceCommand { + } + + private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type component) { +- int i = component.query.applyAsInt(player); +- source.sendSuccess(() -> Component.translatable("commands.experience.query." + component.name, player.getDisplayName(), i), false); +- return i; ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading ++ int i = component.query.applyAsInt(serverPlayer); // Folia - region threading ++ source.sendSuccess(() -> Component.translatable("commands.experience.query." + component.name, serverPlayer.getDisplayName(), i), false); // Folia - region threading ++ }, null, 1L); // Folia - region threading ++ return 0; // Folia - region threading + } + + private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type component) { + for (ServerPlayer serverPlayer : targets) { +- component.add.accept(serverPlayer, amount); ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading ++ component.add.accept(player, amount); // Folia - region threading ++ }, null, 1L); // Folia - region threading + } + + if (targets.size() == 1) { +@@ -159,9 +163,11 @@ public class ExperienceCommand { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- if (component.set.test(serverPlayer, amount)) { +- i++; ++ ++i; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading ++ if (component.set.test(player, amount)) { // Folia - region threading ++ // Folia - region threading + } ++ }, null, 1L); // Folia - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +index 4034f52c4fcedf30b6c79b73f47afe1a97888202..d6f98bbfcb57dade34834fca87b4209c5f5d002f 100644 +--- a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +@@ -106,6 +106,16 @@ public class FillBiomeCommand { + }); + } + ++ // Folia start - region threading ++ private static void sendMessage(Consumer> src, Supplier> supplier) { ++ Either either = supplier.get(); ++ CommandSyntaxException ex = either == null ? null : either.right().orElse(null); ++ if (ex != null) { ++ src.accept(() -> (Component)ex.getRawMessage()); ++ } ++ } ++ // Folia end - region threading ++ + public static Either fill( + ServerLevel world, BlockPos from, BlockPos to, Holder biome, Predicate> filter, Consumer> feedbackConsumer + ) { +@@ -117,6 +127,17 @@ public class FillBiomeCommand { + if (i > j) { + return Either.right(ERROR_VOLUME_TOO_LARGE.create(j, i)); + } else { ++ // Folia start - region threading ++ int buffer = 0; // no buffer, we do not touch neighbours ++ world.moonrise$loadChunksAsync( ++ (boundingBox.minX() - buffer) >> 4, ++ (boundingBox.maxX() + buffer) >> 4, ++ (boundingBox.minZ() - buffer) >> 4, ++ (boundingBox.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ sendMessage(feedbackConsumer, () -> { + List list = new ArrayList<>(); + + for (int k = SectionPos.blockToSectionCoord(boundingBox.minZ()); k <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); k++) { +@@ -153,6 +174,11 @@ public class FillBiomeCommand { + ) + ); + return Either.left(mutableInt.getValue()); ++ // Folia start - region threading ++ }); // sendMessage ++ }); // loadChunksASync ++ return Either.left(Integer.valueOf(0)); ++ // Folia end - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java +index 0509e28f79d13615b5baefc34799b0ad2df071be..d52a31656f2ade6d860cee6bc2f7d92a0588d643 100644 +--- a/src/main/java/net/minecraft/server/commands/FillCommand.java ++++ b/src/main/java/net/minecraft/server/commands/FillCommand.java +@@ -151,6 +151,12 @@ public class FillCommand { + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int fillBlocks( + CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter + ) throws CommandSyntaxException { +@@ -161,6 +167,18 @@ public class FillCommand { + } else { + List list = Lists.newArrayList(); + ServerLevel serverLevel = source.getLevel(); ++ // Folia start - region threading ++ int buffer = 32; ++ // physics may spill into neighbour chunks, so use a buffer ++ serverLevel.moonrise$loadChunksAsync( ++ (range.minX() - buffer) >> 4, ++ (range.maxX() + buffer) >> 4, ++ (range.minZ() - buffer) >> 4, ++ (range.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { // Folia end - region threading + int k = 0; + + for (BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { +@@ -187,8 +205,13 @@ public class FillCommand { + } else { + int l = k; + source.sendSuccess(() -> Component.translatable("commands.fill.success", l), true); +- return k; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); return 0; // Folia end - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +index 255211f47a1678f714b1e04b31298976ada8a781..87275b46c4411fd51d6572ec7b3f924e347d4ed7 100644 +--- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java ++++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +@@ -97,7 +97,17 @@ public class ForceLoadCommand { + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + ChunkPos chunkPos = pos.toChunkPos(); + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); +@@ -109,14 +119,22 @@ public class ForceLoadCommand { + ), + false + ); +- return 1; ++ return; // Folia - region threading + } else { + throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int listForceLoad(CommandSourceStack source) { + ServerLevel serverLevel = source.getLevel(); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + ResourceKey resourceKey = serverLevel.dimension(); + LongSet longSet = serverLevel.getForcedChunks(); + int i = longSet.size(); +@@ -134,20 +152,27 @@ public class ForceLoadCommand { + } else { + source.sendFailure(Component.translatable("commands.forceload.added.none", Component.translationArg(resourceKey.location()))); + } ++ }); // Folia - region threading + +- return i; ++ return 1; // Folia - region threading + } + + private static int removeAll(CommandSourceStack source) { + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + LongSet longSet = serverLevel.getForcedChunks(); + longSet.forEach(chunkPos -> serverLevel.setChunkForced(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos), false)); + source.sendSuccess(() -> Component.translatable("commands.forceload.removed.all", Component.translationArg(resourceKey.location())), true); ++ }); // Folia - region threading + return 0; + } + + private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean forceLoaded) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + int i = Math.min(from.x(), to.x()); + int j = Math.min(from.z(), to.z()); + int k = Math.max(from.x(), to.x()); +@@ -207,11 +232,18 @@ public class ForceLoadCommand { + ); + } + +- return u; ++ return; // Folia - region threading + } + } + } else { + throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +index d1da3600dc07107309b20ebe6e7c0c4da0e8de76..a2075133b0e8ea6299dec0fa5125e4296eb3276a 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -60,15 +60,18 @@ public class GameModeCommand { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + // Paper start - Expand PlayerGameModeChangeEvent + org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); + if (event != null && !event.isCancelled()) { + logGamemodeChange(context.getSource(), serverPlayer, gameMode); +- i++; ++ // Folia - region threading + } else if (event != null && event.cancelMessage() != null) { + context.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); + // Paper end - Expand PlayerGameModeChangeEvent + } ++ }, null, 1L); // Folia - region threading ++ ++i; // Folia - region threading + } + + return i; +diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java +index 0d9de4c61c7b26a6ff37c12fde629161fd0c3d5a..fca164cd59f1f0bf396e82a4c824c8d779b6c640 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -57,6 +57,7 @@ public class GiveCommand { + + l -= i1; + ItemStack itemstack1 = item.createItemStack(i1, false); ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + boolean flag = entityplayer.getInventory().add(itemstack1); + ItemEntity entityitem; + +@@ -75,6 +76,7 @@ public class GiveCommand { + entityitem.setTarget(entityplayer.getUUID()); + } + } ++ }, null, 1L); // Folia - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/KillCommand.java b/src/main/java/net/minecraft/server/commands/KillCommand.java +index e8ab673921c8089a35a2e678d7a6efed1f728cd7..287681a351f49eabd4f480396314a882bee73645 100644 +--- a/src/main/java/net/minecraft/server/commands/KillCommand.java ++++ b/src/main/java/net/minecraft/server/commands/KillCommand.java +@@ -24,7 +24,9 @@ public class KillCommand { + + private static int kill(CommandSourceStack source, Collection targets) { + for (Entity entity : targets) { +- entity.kill(source.getLevel()); ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { // Folia - region threading ++ nmsEntity.kill((net.minecraft.server.level.ServerLevel)nmsEntity.level()); // Folia - region threading ++ }, null, 1L); // Folia - region threading + } + + if (targets.size() == 1) { +diff --git a/src/main/java/net/minecraft/server/commands/PlaceCommand.java b/src/main/java/net/minecraft/server/commands/PlaceCommand.java +index 89f89c0d223961a5aabfe60150893984f5894ff9..7f55ea913e557d99cf575ae4604523ca6d0c1f17 100644 +--- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java ++++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java +@@ -88,12 +88,25 @@ public class PlaceCommand { + }))))))))); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { + ServerLevel worldserver = source.getLevel(); + ConfiguredFeature worldgenfeatureconfigured = (ConfiguredFeature) feature.value(); + ChunkPos chunkcoordintpair = new ChunkPos(pos); + + PlaceCommand.checkLoaded(worldserver, new ChunkPos(chunkcoordintpair.x - 1, chunkcoordintpair.z - 1), new ChunkPos(chunkcoordintpair.x + 1, chunkcoordintpair.z + 1)); ++ // Folia start - region threading ++ worldserver.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + if (!worldgenfeatureconfigured.place(worldserver, worldserver.getChunkSource().getGenerator(), worldserver.getRandom(), pos)) { + throw PlaceCommand.ERROR_FEATURE_FAILED.create(); + } else { +@@ -102,8 +115,16 @@ public class PlaceCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.place.feature.success", s, pos.getX(), pos.getY(), pos.getZ()); + }, true); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeJigsaw(CommandSourceStack source, Holder structurePool, ResourceLocation id, int maxDepth, BlockPos pos) throws CommandSyntaxException { +@@ -111,20 +132,42 @@ public class PlaceCommand { + ChunkPos chunkcoordintpair = new ChunkPos(pos); + + PlaceCommand.checkLoaded(worldserver, chunkcoordintpair, chunkcoordintpair); ++ // Folia start - region threading ++ worldserver.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + if (!JigsawPlacement.generateJigsaw(worldserver, structurePool, id, maxDepth, pos, false)) { + throw PlaceCommand.ERROR_JIGSAW_FAILED.create(); + } else { + source.sendSuccess(() -> { + return Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()); + }, true); +- return 1; ++ return; // Folia start - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { + ServerLevel worldserver = source.getLevel(); + Structure structure1 = (Structure) structure.value(); + ChunkGenerator chunkgenerator = worldserver.getChunkSource().getGenerator(); ++ // Folia start - region threading ++ worldserver.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + StructureStart structurestart = structure1.generate(structure, worldserver.dimension(), source.registryAccess(), chunkgenerator, chunkgenerator.getBiomeSource(), worldserver.getChunkSource().randomState(), worldserver.getStructureManager(), worldserver.getSeed(), new ChunkPos(pos), 0, worldserver, (holder) -> { + return true; + }); +@@ -146,12 +189,27 @@ public class PlaceCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.place.structure.success", s, pos.getX(), pos.getY(), pos.getZ()); + }, true); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeTemplate(CommandSourceStack source, ResourceLocation id, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed) throws CommandSyntaxException { + ServerLevel worldserver = source.getLevel(); ++ // Folia start - region threading ++ worldserver.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + StructureTemplateManager structuretemplatemanager = worldserver.getStructureManager(); + + Optional optional; +@@ -182,9 +240,17 @@ public class PlaceCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.place.template.success", Component.translationArg(id), pos.getX(), pos.getY(), pos.getZ()); + }, true); +- return 1; ++ return; // Folia - region threading + } + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + private static void checkLoaded(ServerLevel world, ChunkPos pos1, ChunkPos pos2) throws CommandSyntaxException { +diff --git a/src/main/java/net/minecraft/server/commands/RecipeCommand.java b/src/main/java/net/minecraft/server/commands/RecipeCommand.java +index dabb43ffe0e9e8aef0aa275c48ab7fdba03762cc..eb55ca5730588d2f7f1d6e80f8cb43a1955c44cb 100644 +--- a/src/main/java/net/minecraft/server/commands/RecipeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/RecipeCommand.java +@@ -81,7 +81,12 @@ public class RecipeCommand { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += serverPlayer.awardRecipes(recipes); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.awardRecipes(recipes); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +@@ -103,7 +108,12 @@ public class RecipeCommand { + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += serverPlayer.resetRecipes(recipes); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.resetRecipes(recipes); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +index 977f3b1f0b6822d37254452c681717d31d3e1011..ea13a3b409b8a4a6a77071c700b4dd0a33b02716 100644 +--- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +@@ -80,10 +80,21 @@ public class SetBlockCommand { + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int setBlock( + CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate condition + ) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ // Folia end - region threading + if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { + throw ERROR_FAILED.create(); + } else { +@@ -102,9 +113,16 @@ public class SetBlockCommand { + } else { + serverLevel.blockUpdated(pos, block.getState().getBlock()); + source.sendSuccess(() -> Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ return; // Folia - region threading + } + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + public interface Filter { +diff --git a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +index 15db9368227dbc29d07d74e85bd126b345b526b6..161ae9c5f9ff0e8cdf3bb3c6bb1d068607f9c2ad 100644 +--- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +@@ -43,7 +43,11 @@ public class SetSpawnCommand { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); + + // Paper start - Add PlayerSetSpawnEvent +- if (entityplayer.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { ++ // Folia start - region threading ++ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); ++ }, null, 1L); ++ if (true) { // Folia end - region threading + actualTargets.add(entityplayer); + } + // Paper end - Add PlayerSetSpawnEvent +diff --git a/src/main/java/net/minecraft/server/commands/SummonCommand.java b/src/main/java/net/minecraft/server/commands/SummonCommand.java +index f635da34335cd2901adf975fcd74c5c6f9785836..fb80b635d279a18758c6b0ce99858326bcade38e 100644 +--- a/src/main/java/net/minecraft/server/commands/SummonCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java +@@ -64,11 +64,18 @@ public class SummonCommand { + if (entity == null) { + throw SummonCommand.ERROR_FAILED.create(); + } else { +- if (initialize && entity instanceof Mob) { +- ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, (SpawnGroupData) null); +- } ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> { ++ if (initialize && entity instanceof Mob) { ++ ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, (SpawnGroupData) null); ++ } ++ worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); ++ } ++ ); ++ // Folia end - region threading + +- if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" ++ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading + throw SummonCommand.ERROR_DUPLICATE_UUID.create(); + } else { + return entity; +diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +index c13b6f14c3061710c2b27034db240cc94ec0fcb5..854897f5fcc8109a69cabc7d4fef1a23289d2733 100644 +--- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java ++++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +@@ -75,7 +75,7 @@ public class TeleportCommand { + while (iterator.hasNext()) { + Entity entity1 = (Entity) iterator.next(); + +- TeleportCommand.performTeleport(source, entity1, (ServerLevel) destination.level(), destination.getX(), destination.getY(), destination.getZ(), EnumSet.noneOf(Relative.class), destination.getYRot(), destination.getXRot(), (LookAt) null); ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity1, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading + } + + if (targets.size() == 1) { +@@ -173,6 +173,24 @@ public class TeleportCommand { + float f4 = Mth.wrapDegrees(f2); + float f5 = Mth.wrapDegrees(f3); + ++ // Folia start - region threading ++ if (true) { ++ ServerLevel worldFinal = world; ++ Vec3 posFinal = new Vec3(x, y, z); ++ Float yawFinal = Float.valueOf(f2); ++ Float pitchFinal = Float.valueOf(f3); ++ target.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ nmsEntity.unRide(); ++ nmsEntity.teleportAsync( ++ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ }, null, 1L); ++ return; ++ } ++ // Folia end - region threading + // CraftBukkit start - Teleport event + boolean result; + if (target instanceof ServerPlayer player) { +diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java +index 8b83d747de831878ff45dc74b4ae7cd9efb21d8c..92214e96d4b174dfac8a1d6bb3a195571aa91bc6 100644 +--- a/src/main/java/net/minecraft/server/commands/TimeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java +@@ -55,6 +55,7 @@ public class TimeCommand { + public static int setTime(CommandSourceStack source, int time) { + Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + +@@ -71,12 +72,14 @@ public class TimeCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.time.set", time); + }, true); +- return TimeCommand.getDayTime(source.getLevel()); ++ }); // Folia - region threading ++ return 0; // Folia - region threading + } + + public static int addTime(CommandSourceStack source, int time) { + Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + +@@ -95,6 +98,7 @@ public class TimeCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.time.set", j); + }, true); +- return j; ++ }); // Folia - region threading ++ return 0; // Folia - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/commands/WeatherCommand.java b/src/main/java/net/minecraft/server/commands/WeatherCommand.java +index 4f5463bc299a43c32fefad6c57aa6a74fe73245b..72ce93dbb8ed9c3063c20fcf1b3cf0b59de8c4cb 100644 +--- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java ++++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java +@@ -38,26 +38,32 @@ public class WeatherCommand { + } + + private static int setClear(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> { + return Component.translatable("commands.weather.set.clear"); + }, true); ++ }); // Folia - region threading + return duration; + } + + private static int setRain(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> { + return Component.translatable("commands.weather.set.rain"); + }, true); ++ }); // Folia - region threading + return duration; + } + + private static int setThunder(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> { + return Component.translatable("commands.weather.set.thunder"); + }, true); ++ }); // Folia - region threading + return duration; + } + } +diff --git a/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java b/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java +index 812f2adc6fc20aa126e629284fe594a923b24540..0a5e6961fb37e9a53cd39b1bd233e0204986b244 100644 +--- a/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java ++++ b/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java +@@ -56,7 +56,17 @@ public class WorldBorderCommand { + }))))); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + + if (worldborder.getDamageSafeZone() == (double) distance) { +@@ -66,11 +76,22 @@ public class WorldBorderCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.damage.buffer.success", String.format(Locale.ROOT, "%.2f", distance)); + }, true); +- return (int) distance; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + + if (worldborder.getDamagePerBlock() == (double) damagePerBlock) { +@@ -80,11 +101,22 @@ public class WorldBorderCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.damage.amount.success", String.format(Locale.ROOT, "%.2f", damagePerBlock)); + }, true); +- return (int) damagePerBlock; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + + if (worldborder.getWarningTime() == time) { +@@ -94,11 +126,22 @@ public class WorldBorderCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.warning.time.success", time); + }, true); +- return time; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + + if (worldborder.getWarningBlocks() == distance) { +@@ -108,20 +151,38 @@ public class WorldBorderCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.warning.distance.success", distance); + }, true); +- return distance; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int getSize(CommandSourceStack source) { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ // Folia end - region threading + double d0 = source.getLevel().getWorldBorder().getSize(); // CraftBukkit + + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", d0)); + }, false); +- return Mth.floor(d0 + 0.5D); ++ return; // Folia - region threading ++ // Folia start - region threading ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + + if (worldborder.getCenterX() == (double) pos.x && worldborder.getCenterZ() == (double) pos.y) { +@@ -131,13 +192,24 @@ public class WorldBorderCommand { + source.sendSuccess(() -> { + return Component.translatable("commands.worldborder.center.success", String.format(Locale.ROOT, "%.2f", pos.x), String.format(Locale.ROOT, "%.2f", pos.y)); + }, true); +- return 0; ++ return; // Folia - region threading + } else { + throw WorldBorderCommand.ERROR_TOO_FAR_OUT.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setSize(CommandSourceStack source, double distance, long time) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit + double d1 = worldborder.getSize(); + +@@ -166,7 +238,14 @@ public class WorldBorderCommand { + }, true); + } + +- return (int) (distance - d1); ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 17a158ff6ce6520b69a5a0032ba4c05449dd0cf8..78f33298e809a7f6d079d9f2c64e2caa47a1b25a 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -451,7 +451,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + @Override + public void tickConnection() { + super.tickConnection(); +- this.handleConsoleInputs(); ++ // Folia - region threading + } + + @Override +@@ -768,7 +768,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + + public String runCommand(RconConsoleSource rconConsoleSource, String s) { + rconConsoleSource.prepareForCommand(); +- this.executeBlocking(() -> { ++ final java.util.concurrent.atomic.AtomicReference command = new java.util.concurrent.atomic.AtomicReference<>(s); // Folia start - region threading ++ Runnable sync = () -> { // Folia - region threading + CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); + RemoteServerCommandEvent event = new RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); + this.server.getPluginManager().callEvent(event); +@@ -777,7 +778,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); + this.server.dispatchServerCommand(event.getSender(), serverCommand); +- }); ++ }; // Folia start - region threading ++ java.util.concurrent.CompletableFuture ++ .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask) ++ .whenComplete((Void r, Throwable t) -> { ++ if (t != null) { ++ LOGGER.error("Error handling command for rcon: " + s, t); ++ } ++ }) ++ .join(); ++ // Folia end - region threading + return rconConsoleSource.getCommandResponse(); + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index cfeeddf2cb4ff50dbc29c6913e78ca1dee076790..1668011de11a5ed513815fa1b547ff3a7636cc13 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -139,8 +139,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public final ChunkMap.ChunkDistanceManager distanceManager; + public final AtomicInteger tickingGenerated; // Paper - public + private final String storageName; +- private final PlayerMap playerMap; +- public final Int2ObjectMap entityMap; ++ //private final PlayerMap playerMap; // Folia - region threading ++ //public final Int2ObjectMap entityMap; // Folia - region threading + private final Long2ByteMap chunkTypeCache; + // Paper - rewrite chunk system + public int serverViewDistance; +@@ -185,8 +185,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper - rewrite chunk system + this.toDrop = new LongOpenHashSet(); + this.tickingGenerated = new AtomicInteger(); +- this.playerMap = new PlayerMap(); +- this.entityMap = new Int2ObjectOpenHashMap(); ++ //this.playerMap = new PlayerMap(); // Folia - region threading ++ //this.entityMap = new Int2ObjectOpenHashMap(); // Folia - region threading + this.chunkTypeCache = new Long2ByteOpenHashMap(); + // Paper - rewrite chunk system + Path path = session.getDimensionPath(world.dimension()); +@@ -796,13 +796,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + void updatePlayerStatus(ServerPlayer player, boolean added) { + boolean flag1 = this.skipPlayer(player); +- boolean flag2 = this.playerMap.ignoredOrUnknown(player); ++ // Folia - region threading + + if (added) { +- this.playerMap.addPlayer(player, flag1); ++ // Folia - region threading + this.updatePlayerPos(player); + if (!flag1) { +- this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); ++ // Folia - region threading + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation + } + +@@ -811,9 +811,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } else { + SectionPos sectionposition = player.getLastSectionPos(); + +- this.playerMap.removePlayer(player); +- if (!flag2) { +- this.distanceManager.removePlayer(sectionposition, player); ++ // Folia - region threading ++ if (true) { // Folia - region threading ++ // Folia - region threading + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation + } + +@@ -833,28 +833,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + SectionPos sectionposition = player.getLastSectionPos(); + SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); +- boolean flag = this.playerMap.ignored(player); ++ // Folia - region threading + boolean flag1 = this.skipPlayer(player); +- boolean flag2 = sectionposition.asLong() != sectionposition1.asLong(); ++ // Folia - region threading + +- if (flag2 || flag != flag1) { ++ if (true) { // Folia - region threading + this.updatePlayerPos(player); +- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation +- if (!flag) { +- this.distanceManager.removePlayer(sectionposition, player); +- } +- +- if (!flag1) { +- this.distanceManager.addPlayer(sectionposition1, player); +- } +- +- if (!flag && flag1) { +- this.playerMap.ignorePlayer(player); +- } +- +- if (flag && !flag1) { +- this.playerMap.unIgnorePlayer(player); +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading ++ // Folia - region threading + + // Paper - rewrite chunk system + } +@@ -885,9 +871,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public void addEntity(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot + // Paper start - ignore and warn about illegal addEntity calls instead of crashing server +- if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) { ++ if (!entity.valid || entity.level() != this.level || entity.moonrise$getTrackedEntity() != null) { // Folia - region threading + LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() +- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); ++ + ": " + entity + (entity.moonrise$getTrackedEntity() != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); // Folia - region threading + return; + } + // Paper end - ignore and warn about illegal addEntity calls instead of crashing server +@@ -900,32 +886,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (i != 0) { + int j = entitytypes.updateInterval(); + +- if (this.entityMap.containsKey(entity.getId())) { ++ if (entity.moonrise$getTrackedEntity() != null) { // Folia - region threading + throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); + } else { + ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas()); + +- this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); ++ // Folia - region threading + // Paper start - optimise entity tracker + if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { + throw new IllegalStateException("Entity is already tracked"); + } + ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker); + // Paper end - optimise entity tracker +- playerchunkmap_entitytracker.updatePlayers(this.level.players()); ++ playerchunkmap_entitytracker.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading + if (entity instanceof ServerPlayer) { + ServerPlayer entityplayer = (ServerPlayer) entity; + + this.updatePlayerStatus(entityplayer, true); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- if (playerchunkmap_entitytracker1.entity != entityplayer) { +- playerchunkmap_entitytracker1.updatePlayer(entityplayer); ++ // Folia start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().trackerEntities) { ++ if (possible.moonrise$getTrackedEntity() != null) { ++ possible.moonrise$getTrackedEntity().updatePlayer(entityplayer); + } + } ++ // Folia end - region threading + } + + } +@@ -937,16 +921,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot + if (entity instanceof ServerPlayer entityplayer) { + this.updatePlayerStatus(entityplayer, false); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- playerchunkmap_entitytracker.removePlayer(entityplayer); ++ // Folia start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { ++ if (possible.moonrise$getTrackedEntity() != null) { ++ possible.moonrise$getTrackedEntity().removePlayer(entityplayer); ++ } + } ++ // Folia end - region threading + } + +- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = entity.moonrise$getTrackedEntity(); // Folia - region threading + + if (playerchunkmap_entitytracker1 != null) { + playerchunkmap_entitytracker1.broadcastRemoved(); +@@ -957,9 +941,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimise entity tracker + private void newTrackerTick() { ++ final io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; + +- final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; ++ final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = worldData.trackerEntities; // Folia - region threading + final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); + for (int i = 0, len = trackerEntities.size(); i < len; ++i) { + final Entity entity = trackerEntitiesRaw[i]; +@@ -985,47 +970,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - optimise entity tracker + // Paper - rewrite chunk system + +- List list = Lists.newArrayList(); +- List list1 = this.level.players(); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- ChunkMap.TrackedEntity playerchunkmap_entitytracker; +- +- while (objectiterator.hasNext()) { +- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos; +- SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity); +- boolean flag = !Objects.equals(sectionposition, sectionposition1); +- +- if (flag) { +- playerchunkmap_entitytracker.updatePlayers(list1); +- Entity entity = playerchunkmap_entitytracker.entity; +- +- if (entity instanceof ServerPlayer) { +- list.add((ServerPlayer) entity); +- } +- +- playerchunkmap_entitytracker.lastSectionPos = sectionposition1; +- } +- +- if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) { +- playerchunkmap_entitytracker.serverEntity.sendChanges(); +- } +- } +- +- if (!list.isEmpty()) { +- objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- playerchunkmap_entitytracker.updatePlayers(list); +- } +- } ++ // Folia - region threading + + } + + public void broadcast(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.moonrise$getTrackedEntity(); // Folia - region threading + + if (playerchunkmap_entitytracker != null) { + playerchunkmap_entitytracker.broadcast(packet); +@@ -1034,7 +984,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected void broadcastAndSend(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.moonrise$getTrackedEntity(); // Folia - region threading + + if (playerchunkmap_entitytracker != null) { + playerchunkmap_entitytracker.broadcastAndSend(packet); +@@ -1287,9 +1237,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); + // Paper end - Configurable entity tracking range by Y ++ // Folia start - region threading ++ if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { ++ flag = false; ++ } ++ // Folia end - region threading + + // CraftBukkit start - respect vanish API +- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits ++ if (flag && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading + flag = false; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 746f61661e22d22f2acbbe54a5933e57fbca45b2..f6015f2ac77aeddbd900226d183bf24c93112b21 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -59,16 +59,16 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches + } + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration optimisation +- private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); ++ // Folia - move to regionized world data + + @Override + public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) { +- this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading + } + + @Override + public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { +- this.spawnChunkTracker.remove(player); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading + } + + @Override +@@ -76,9 +76,9 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches + final SectionPos oldPos, final SectionPos newPos, + final boolean oldIgnore, final boolean newIgnore) { + if (newIgnore) { +- this.spawnChunkTracker.remove(player); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading + } else { +- this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading + } + } + // Paper end - chunk tick iteration optimisation +@@ -217,15 +217,15 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches + } + + public int getNaturalSpawnChunkCount() { +- return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public boolean hasPlayersNearby(long chunkPos) { +- return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public LongIterator getSpawnCandidateChunks() { +- return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public String getDebugStatus() { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 1c87904bb99cc40bafc9357fb2fc1703b759c3df..1a43c6fba5eea514dd6cf406f600dc254282da35 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -62,18 +62,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; + public final ChunkMap chunkMap; + private final DimensionDataStorage dataStorage; +- private long lastInhabitedUpdate; ++ //private long lastInhabitedUpdate; // Folia - region threading + public boolean spawnEnemies = true; + public boolean spawnFriendlies = true; + private static final int CACHE_SIZE = 4; + private final long[] lastChunkPos = new long[4]; + private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; + private final ChunkAccess[] lastChunk = new ChunkAccess[4]; +- private final List tickingChunks = new ArrayList(); +- private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet(); +- @Nullable +- @VisibleForDebug +- private NaturalSpawner.SpawnState lastSpawnState; ++ // Folia - moved to regionised world data + // Paper start + private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); + public int getFullChunksCount() { +@@ -99,6 +95,11 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { ++ // Folia start - region threading ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Cannot asynchronously load chunks"); ++ } ++ // Folia end - region threading + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final CompletableFuture completable = new CompletableFuture<>(); + chunkTaskScheduler.scheduleChunkLoad( +@@ -329,6 +330,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + boolean flag1 = Thread.currentThread() == this.mainThread; + CompletableFuture completablefuture; + +@@ -482,16 +484,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + private void tickChunks() { +- long i = this.level.getGameTime(); +- long j = i - this.lastInhabitedUpdate; ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading ++ //long i = this.level.getGameTime(); // Folia - region threading ++ long j = 1L; // Folia - region threading + +- this.lastInhabitedUpdate = i; ++ //this.lastInhabitedUpdate = i; // Folia - region threading + if (!this.level.isDebug()) { + ProfilerFiller gameprofilerfiller = Profiler.get(); + + gameprofilerfiller.push("pollingChunks"); + if (this.level.tickRateManager().runsNormally()) { +- List list = this.tickingChunks; ++ List list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading + + try { + gameprofilerfiller.push("filteringTickingChunks"); +@@ -514,8 +517,9 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + private void broadcastChangedChunks(ProfilerFiller profiler) { ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading + profiler.push("broadcast"); +- Iterator iterator = this.chunkHoldersToBroadcast.iterator(); ++ Iterator iterator = regionizedWorldData.chunkHoldersToBroadcast.iterator(); // Folia - region threading - note: do not need to thread check, as getChunkToSend is only non-null when the chunkholder is loaded + + while (iterator.hasNext()) { + ChunkHolder playerchunk = (ChunkHolder) iterator.next(); +@@ -526,14 +530,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + } + +- this.chunkHoldersToBroadcast.clear(); ++ regionizedWorldData.chunkHoldersToBroadcast.clear(); // Folia - region threading + profiler.pop(); + } + + private void collectTickingChunks(List chunks) { + // Paper start - chunk tick iteration optimisation + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = +- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); ++ this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading + + final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); + final int size = tickingChunks.size(); +@@ -554,6 +558,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + private void tickChunks(ProfilerFiller profiler, long timeDelta, List chunks) { ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading + profiler.popPush("naturalSpawnCount"); + int j = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - Optional per player mob spawns +@@ -561,7 +566,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + NaturalSpawner.SpawnState spawnercreature_d; // moved down + if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + // re-set mob counts +- for (ServerPlayer player : this.level.players) { ++ for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading + // Paper start - per player mob spawning backoff + for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { + player.mobCounts[ii] = 0; +@@ -574,28 +579,28 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + // Paper end - per player mob spawning backoff + } +- spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } else { +- spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } + // Paper end - Optional per player mob spawns + +- this.lastSpawnState = spawnercreature_d; ++ regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading + profiler.popPush("spawnAndTick"); +- boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit ++ boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading + int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + List list1; + + if (flag && (this.spawnEnemies || this.spawnFriendlies)) { + // Paper start - PlayerNaturallySpawnCreaturesEvent +- for (ServerPlayer entityPlayer : this.level.players()) { ++ for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading + int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance()); + chunkRange = Math.min(chunkRange, 8); + entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); + entityPlayer.playerNaturallySpawnedEvent.callEvent(); + } + // Paper end - PlayerNaturallySpawnCreaturesEvent +- boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit ++ boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading + + list1 = NaturalSpawner.getFilteredSpawningCategories(spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1, this.level); // CraftBukkit + } else { +@@ -669,21 +674,26 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(ChunkPos.asLong(i, j)); + + if (playerchunk != null && playerchunk.blockChanged(pos)) { +- this.chunkHoldersToBroadcast.add(playerchunk); ++ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(playerchunk); // Folia - region threading + } + + } + + @Override + public void onLightUpdate(LightLayer type, SectionPos pos) { +- this.mainThreadProcessor.execute(() -> { ++ Runnable run = () -> { // Folia - region threading + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.chunk().toLong()); + + if (playerchunk != null && playerchunk.sectionLightChanged(type, pos.y())) { +- this.chunkHoldersToBroadcast.add(playerchunk); ++ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(playerchunk); // Folia - region threading + } + +- }); ++ }; // Folia - region threading ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this.level, pos.getX(), pos.getZ(), run ++ ); ++ // Folia end - region threading + } + + public void addRegionTicket(TicketType ticketType, ChunkPos pos, int radius, T argument) { +@@ -767,7 +777,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + @Nullable + @VisibleForDebug + public NaturalSpawner.SpawnState getLastSpawnState() { +- return this.lastSpawnState; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading ++ return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading + } + + public void removeTicketsOnClosing() { +@@ -776,7 +787,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + + public void onChunkReadyToSend(ChunkHolder chunkHolder) { + if (chunkHolder.hasChangesToBroadcast()) { +- this.chunkHoldersToBroadcast.add(chunkHolder); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + } +@@ -814,8 +825,59 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + return ServerChunkCache.this.mainThread; + } + ++ // Folia start - region threading ++ @Override ++ public CompletableFuture submit(Supplier task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public CompletableFuture submit(Runnable task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public void schedule(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.schedule(runnable); ++ } ++ ++ @Override ++ public void executeBlocking(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeBlocking(runnable); ++ } ++ ++ @Override ++ public void execute(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.execute(runnable); ++ } ++ ++ @Override ++ public void executeIfPossible(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeIfPossible(runnable); ++ } ++ // Folia end - region threading ++ + @Override + protected void doRunTask(Runnable task) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + Profiler.get().incrementCounter("runTask"); + super.doRunTask(task); + } +@@ -823,12 +885,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { ++ // Folia start - region threading ++ if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) { ++ throw new IllegalStateException("Polling tasks from non-owned region"); ++ } ++ // Folia end - region threading + // Paper start - rewrite chunk system + final ServerChunkCache serverChunkCache = ServerChunkCache.this; + if (serverChunkCache.runDistanceManagerUpdates()) { + return true; + } else { +- return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); ++ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Folia - region threading + } + // Paper end - rewrite chunk system + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/server/level/ServerEntityGetter.java b/src/main/java/net/minecraft/server/level/ServerEntityGetter.java +index 296059746fe9f5c35fedd8ca1dea488da519ac05..7df954d6375abb83cb0b140a16336b478d13e9d8 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntityGetter.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntityGetter.java +@@ -14,17 +14,17 @@ public interface ServerEntityGetter extends EntityGetter { + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity) { +- return this.getNearestEntity(this.players(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); // Folia - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, x, y, z); // Folia - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetPredicate, null, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, null, x, y, z); // Folia - region threading + } + + @Nullable +@@ -57,7 +57,7 @@ public interface ServerEntityGetter extends EntityGetter { + default List getNearbyPlayers(TargetingConditions targetPredicate, LivingEntity entity, AABB box) { + List list = new ArrayList<>(); + +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (box.contains(player.getX(), player.getY(), player.getZ()) && targetPredicate.test(this.getLevel(), entity, player)) { + list.add(player); + } +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 1f898500d0e9b18a880645ceb0a8ff0fe75f4e48..e0ad5a7715949c281a94f000e2df5cb2a0a99dff 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -196,42 +196,40 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int EMPTY_TIME_NO_TICK = 300; + private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; +- final List players = Lists.newArrayList(); ++ final List players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading + public final ServerChunkCache chunkSource; + private final MinecraftServer server; + public final PrimaryLevelData serverLevelData; // CraftBukkit - type + private int lastSpawnChunkRadius; +- final EntityTickList entityTickList = new EntityTickList(); ++ //final EntityTickList entityTickList = new EntityTickList(); // Folia - region threading + // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; + private int emptyTime; + private final PortalForcer portalForcer; +- private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); +- private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); +- private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); +- final Set navigatingMobs = new ObjectOpenHashSet(); ++ //private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading ++ //private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading ++ //private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); // Folia - region threading ++ //final Set navigatingMobs = new ObjectOpenHashSet(); // Folia - region threading + volatile boolean isUpdatingNavigations; + protected final Raids raids; +- private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet(); +- private final List blockEventsToReschedule = new ArrayList(64); +- private boolean handlingTick; ++ //private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet(); // Folia - region threading ++ //private final List blockEventsToReschedule = new ArrayList(64); // Folia - region threading ++ //private boolean handlingTick; // Folia - region threading + private final List customSpawners; + @Nullable + private EndDragonFight dragonFight; + final Int2ObjectMap dragonParts = new Int2ObjectOpenHashMap(); + private final StructureManager structureManager; + private final StructureCheck structureCheck; +- private final boolean tickTime; ++ public final boolean tickTime; // Folia - region threading + private final RandomSequences randomSequences; + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess convertable; + public final UUID uuid; +- public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent +- public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent +- private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) ++ // Folia - region threading - move to regionised world data + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -259,6 +257,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + ++ // Folia start - region threading ++ // don't let players move into regions not owned ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) { ++ return false; ++ } ++ // Folia end - region threading ++ + ServerChunkCache chunkProvider = this.getChunkSource(); + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { +@@ -314,11 +319,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; + private long lastMidTickFailure; + private long tickedBlocksOrFluids; +- private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); +- private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; +- private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); +- private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); +- private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); ++ // Folia - region threading - move to regionized data + + @Override + public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { +@@ -376,7 +377,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public final int moonrise$getRegionChunkShift() { +- return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); ++ return this.regioniser.sectionChunkShift; // Folia - region threading + } + + @Override +@@ -475,22 +476,22 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() { +- return this.nearbyPlayers; ++ return this.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getLoadedChunks() { +- return this.loadedChunks; ++ return this.getCurrentWorldData().getChunks(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { +- return this.tickingChunks; ++ return this.getCurrentWorldData().getTickingChunks(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { +- return this.entityTickingChunks; ++ return this.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading + } + + @Override +@@ -510,91 +511,96 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration + private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; +- private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); +- private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); ++ // Folia - region threading + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getPlayerTickingChunks() { +- return this.playerTickingChunks; ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + @Override + public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { +- final ChunkPos pos = chunk.getPos(); +- if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { +- return; +- } +- +- this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ // Folia - region threading + } + + @Override + public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { +- this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ // Folia - region threading + } + + @Override + public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { +- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); +- +- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); +- +- if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { +- // already added +- return; +- } +- +- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() +- .chunkHolderManager.getChunkHolder(chunkKey); +- +- if (chunkHolder == null || !chunkHolder.isTickingReady()) { +- return; +- } +- +- this.playerTickingChunks.add( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() +- ); ++ // Folia - region threading + } + + @Override + public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { +- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); +- +- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); +- final int val = this.playerTickingRequests.addTo(chunkKey, -1); +- +- if (val <= 0) { +- throw new IllegalStateException("Negative counter"); +- } +- +- if (val != 1) { +- // still has at least one request +- return; +- } +- +- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() +- .chunkHolderManager.getChunkHolder(chunkKey); +- +- if (chunkHolder == null || !chunkHolder.isTickingReady()) { +- return; +- } +- +- this.playerTickingChunks.remove( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() +- ); ++ // Folia - region threading + } + // Paper end - chunk tick iteration + // Paper start - lag compensation + private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; + + public long getLagCompensationTick() { +- return this.lagCompensationTick; ++ return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading + } + + public void updateLagCompensationTick() { +- this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + // Paper end - lag compensation ++ // Folia start - region threading ++ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); ++ public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; ++ { ++ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( ++ (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))), ++ (1.0 / 6.0), ++ Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())), ++ 1, ++ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), ++ this, ++ this.tickRegions ++ ); ++ } ++ public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); ++ public static final int WORLD_INIT_NOT_CHECKED = 0; ++ public static final int WORLD_INIT_CHECKING = 1; ++ public static final int WORLD_INIT_CHECKED = 2; ++ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); ++ public ChunkPos randomSpawnSelection; ++ ++ public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} ++ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); ++ ++ public void pushPendingTeleport(final PendingTeleport teleport) { ++ synchronized (this.pendingTeleports) { ++ this.pendingTeleports.add(teleport); ++ } ++ } ++ ++ public boolean removePendingTeleport(final PendingTeleport teleport) { ++ synchronized (this.pendingTeleports) { ++ return this.pendingTeleports.remove(teleport); ++ } ++ } ++ ++ public List removeAllRegionTeleports() { ++ final List ret = new ArrayList<>(); ++ ++ synchronized (this.pendingTeleports) { ++ for (final Iterator iterator = this.pendingTeleports.iterator(); iterator.hasNext(); ) { ++ final PendingTeleport pendingTeleport = iterator.next(); ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) { ++ ret.add(pendingTeleport); ++ iterator.remove(); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ // Folia end - region threading + + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +@@ -639,7 +645,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + }); + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); + this.portalForcer = new PortalForcer(this); +- this.updateSkyBrightness(); ++ //this.updateSkyBrightness(); // Folia - region threading - delay until first tick + this.prepareWeather(); + this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize()); + this.raids = (Raids) this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration())); +@@ -677,7 +683,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); + // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked ++ } ++ ++ // Folia start - region threading ++ public void updateTickData() { ++ this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); + } ++ // Folia end - region threading + + // Paper start + @Override +@@ -706,60 +719,44 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(biomeX, biomeY, biomeZ, this.getChunkSource().randomState().sampler()); + } + ++ @Override // Folia - region threading + public StructureManager structureManager() { + return this.structureManager; + } + +- public void tick(BooleanSupplier shouldKeepTicking) { ++ public void tick(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking + ProfilerFiller gameprofilerfiller = Profiler.get(); + +- this.handlingTick = true; ++ regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking + TickRateManager tickratemanager = this.tickRateManager(); + boolean flag = tickratemanager.runsNormally(); + + if (flag) { + gameprofilerfiller.push("world border"); +- this.getWorldBorder().tick(); ++ if (region == null) this.getWorldBorder().tick(); // Folia - regionised ticking + gameprofilerfiller.popPush("weather"); +- this.advanceWeatherCycle(); ++ if (region == null) this.advanceWeatherCycle(); // Folia - regionised ticking + gameprofilerfiller.pop(); + } + +- int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); ++ //int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); // Folia - region threading - move into tickSleep + long j; + +- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { +- // CraftBukkit start +- j = this.levelData.getDayTime() + 24000L; +- TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); +- if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { +- this.getCraftServer().getPluginManager().callEvent(event); +- if (!event.isCancelled()) { +- this.setDayTime(this.getDayTime() + event.getSkipAmount()); +- } +- } +- +- if (!event.isCancelled()) { +- this.wakeUpAllPlayers(); +- } +- // CraftBukkit end +- if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { +- this.resetWeatherCycle(); +- } +- } ++ if (region == null) this.tickSleep(); // Folia - region threading + +- this.updateSkyBrightness(); ++ if (region == null) this.updateSkyBrightness(); // Folia - region threading + if (flag) { + this.tickTime(); + } + + gameprofilerfiller.push("tickPending"); + if (!this.isDebug() && flag) { +- j = this.getGameTime(); ++ j = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading + gameprofilerfiller.push("blockTicks"); +- this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks ++ regionizedWorldData.getBlockLevelTicks().tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking + gameprofilerfiller.popPush("fluidTicks"); +- this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks ++ regionizedWorldData.getFluidLevelTicks().tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking + gameprofilerfiller.pop(); + } + +@@ -775,9 +772,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.runBlockEvents(); + } + +- this.handlingTick = false; ++ regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking + gameprofilerfiller.pop(); +- boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this ++ boolean flag1 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this // Folia - unrestore this, we always need to tick empty worlds + + if (flag1) { + this.resetEmptyTime(); +@@ -786,17 +783,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + if (flag1 || this.emptyTime++ < 300) { + gameprofilerfiller.push("entities"); + if (this.dragonFight != null && flag) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading + gameprofilerfiller.push("dragonFight"); + this.dragonFight.tick(); + gameprofilerfiller.pop(); ++ } else { // Folia start - region threading ++ // try to load dragon fight ++ ChunkPos fightCenter = new ChunkPos(this.dragonFight.origin); ++ this.chunkSource.addTicketAtLevel( ++ TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ fightCenter ++ ); ++ } // Folia end - region threading + } + + org.spigotmc.ActivationRange.activateEntities(this); // Spigot +- this.entityTickList.forEach((entity) -> { ++ regionizedWorldData.forEachTickingEntity((entity) -> { // Folia - regionised ticking + if (!entity.isRemoved()) { + if (!tickratemanager.isEntityFrozen(entity)) { + gameprofilerfiller.push("checkDespawn"); + entity.checkDespawn(); ++ if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! + gameprofilerfiller.pop(); + if (true) { // Paper - rewrite chunk system + Entity entity1 = entity.getVehicle(); +@@ -825,6 +832,31 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + gameprofilerfiller.pop(); + } + ++ // Folia start - region threading ++ public void tickSleep() { ++ int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); long j; // Folia moved from tick loop ++ if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { ++ // CraftBukkit start ++ j = this.levelData.getDayTime() + 24000L; ++ TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); ++ if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ this.getCraftServer().getPluginManager().callEvent(event); ++ if (!event.isCancelled()) { ++ this.setDayTime(this.getDayTime() + event.getSkipAmount()); ++ } ++ } ++ ++ if (!event.isCancelled()) { ++ this.wakeUpAllPlayers(); ++ } ++ // CraftBukkit end ++ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { ++ this.resetWeatherCycle(); ++ } ++ } ++ } ++ // Folia end - region threading ++ + @Override + public boolean shouldTickBlocksAt(long chunkPos) { + // Paper start - rewrite chunk system +@@ -835,13 +867,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + protected void tickTime() { + if (this.tickTime) { +- long i = this.levelData.getGameTime() + 1L; ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading ++ long i = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading + +- this.serverLevelData.setGameTime(i); ++ regionizedWorldData.setRedstoneGameTime(i); // Folia - region threading + Profiler.get().push("scheduledFunctions"); +- this.serverLevelData.getScheduledEvents().tick(this.server, i); ++ if (false) this.serverLevelData.getScheduledEvents().tick(this.server, i); // Folia - region threading - TODO any way to bring this in? + Profiler.get().pop(); +- if (this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ if (false && this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading + this.setDayTime(this.levelData.getDayTime() + 1L); + } + +@@ -866,17 +899,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private void wakeUpAllPlayers() { + this.sleepStatus.removeAllSleepers(); + (this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> { // CraftBukkit - decompile error +- entityplayer.stopSleepInBed(false, false); ++ // Folia start - region threading ++ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ if (player.level() != ServerLevel.this || !player.isSleeping()) { ++ return; ++ } ++ player.stopSleepInBed(false, false); ++ }, null, 1L); ++ // Folia end - region threading + }); + } + + // Paper start - optimise random ticking +- private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); ++ private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading + + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); +- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; ++ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Folia - region threading + final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); + + final ChunkPos cpos = chunk.getPos(); +@@ -923,7 +963,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper end - optimise random ticking + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { +- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking ++ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Paper - optimise random ticking // Folia - region threading + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); +@@ -1061,7 +1101,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + public boolean isHandlingTick() { +- return this.handlingTick; ++ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking + } + + public boolean canSleepThroughNights() { +@@ -1093,6 +1133,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + public void updateSleepingPlayerList() { ++ // Folia start - region threading ++ if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ ServerLevel.this.updateSleepingPlayerList(); ++ }); ++ return; ++ } ++ // Folia end - region threading + if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { + this.announceSleepStatus(); + } +@@ -1104,7 +1152,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return this.server.getScoreboard(); + } + +- private void advanceWeatherCycle() { ++ public void advanceWeatherCycle() { // Folia - region threading - public + boolean flag = this.isRaining(); + + if (this.dimensionType().hasSkyLight()) { +@@ -1190,23 +1238,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel)); + } + // */ +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level() == this) { +- ((ServerPlayer) this.players.get(idx)).tickWeather(); ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading ++ for (ServerPlayer player : players) { // Folia - region threading ++ if (player.level() == this) { // Folia - region threading ++ player.tickWeather(); // Folia - region threading + } + } + + if (flag != this.isRaining()) { + // Only send weather packets to those affected +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level() == this) { +- ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); ++ for (ServerPlayer player : players) { // Folia - region threading ++ if (player.level() == this) { // Folia - region threading ++ player.setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); // Folia - region threading + } + } + } +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level() == this) { +- ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); ++ for (ServerPlayer player : players) { // Folia - region threading ++ if (player.level() == this) { // Folia - region threading ++ player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); // Folia - region threading + } + } + // CraftBukkit end +@@ -1268,13 +1317,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + // Paper start - log detailed entity tick information + // TODO replace with varhandle +- static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); ++ // Folia - region threading + + public static List getCurrentlyTickingEntities() { +- Entity ticking = currentlyTickingEntity.get(); +- List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); +- +- return ret; ++ throw new UnsupportedOperationException(); // Folia - region threading + } + // Paper end - log detailed entity tick information + +@@ -1282,9 +1328,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper start - log detailed entity tick information + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); + try { +- if (currentlyTickingEntity.get() == null) { +- currentlyTickingEntity.lazySet(entity); +- } ++ // Folia - region threading + // Paper end - log detailed entity tick information + // Spigot start + /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2 +@@ -1304,7 +1348,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); // Paper - EAR 2 + if (isActive) { // Paper - EAR 2 + entity.tick(); +- entity.postTick(); // CraftBukkit ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ // removed from region while ticking ++ return; ++ } ++ if (entity.handlePortal()) { ++ // portalled ++ return; ++ } ++ // Folia end - region threading + } else { entity.inactiveTick(); } // Paper - EAR 2 + gameprofilerfiller.pop(); + Iterator iterator = entity.getPassengers().iterator(); +@@ -1317,16 +1370,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + // Paper start - log detailed entity tick information + } finally { +- if (currentlyTickingEntity.get() == entity) { +- currentlyTickingEntity.lazySet(null); +- } ++ // Folia - region threading + } + // Paper end - log detailed entity tick information + } + + 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)) { ++ if (passenger instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passenger)) { // Folia - region threading + passenger.setOldPosAndRot(); + ++passenger.tickCount; + ProfilerFiller gameprofilerfiller = Profiler.get(); +@@ -1338,7 +1389,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper start - EAR 2 + if (isActive) { + passenger.rideTick(); +- passenger.postTick(); // CraftBukkit ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passenger)) { ++ // removed from region while ticking ++ return; ++ } ++ if (passenger.handlePortal()) { ++ // portalled ++ return; ++ } ++ // Folia end - region threading + } else { + passenger.setDeltaMovement(Vec3.ZERO); + passenger.inactiveTick(); +@@ -1423,19 +1483,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // Paper end - add close param + +- // CraftBukkit start - moved from MinecraftServer.saveChunks +- ServerLevel worldserver1 = this; +- +- this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); +- this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); +- this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); +- // CraftBukkit end ++ // Folia - move into saveLevelData + } + +- private void saveLevelData(boolean flush) { ++ public void saveLevelData(boolean flush) { // Folia - public + if (this.dragonFight != null) { + this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit + } ++ // Folia start - moved into saveLevelData ++ ServerLevel worldserver1 = this; ++ ++ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); ++ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); ++ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); ++ // Folia end - moved into saveLevelData + + DimensionDataStorage worldpersistentdata = this.getChunkSource().getDataStorage(); + +@@ -1497,6 +1558,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return list; + } + ++ // Folia start - region threading ++ @Nullable ++ public ServerPlayer getRandomLocalPlayer() { ++ List list = this.getLocalPlayers(); ++ list = new java.util.ArrayList<>(list); ++ list.removeIf((ServerPlayer player) -> { ++ return !player.isAlive(); ++ }); ++ ++ return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); ++ } ++ // Folia end - region threading ++ + @Nullable + public ServerPlayer getRandomPlayer() { + List list = this.getPlayers(LivingEntity::isAlive); +@@ -1581,8 +1655,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } else { + if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added + // Paper start - capture all item additions to the world +- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { +- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); ++ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading ++ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading + return true; + } + // Paper end - capture all item additions to the world +@@ -1751,21 +1825,22 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { +- if (this.isUpdatingNavigations) { ++ if (false && this.isUpdatingNavigations) { // Folia - region threading + String s = "recursive call to sendBlockUpdated"; + + Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); + } + + this.getChunkSource().blockChanged(pos); +- this.pathTypesByPosCache.invalidate(pos); ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading ++ regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading + if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates + VoxelShape voxelshape = oldState.getCollisionShape(this, pos); + VoxelShape voxelshape1 = newState.getCollisionShape(this, pos); + + if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { + List list = new ObjectArrayList(); +- Iterator iterator = this.navigatingMobs.iterator(); ++ Iterator iterator = regionizedWorldData.getNavigatingMobs(); // Folia - region threading + + while (iterator.hasNext()) { + // CraftBukkit start - fix SPIGOT-6362 +@@ -1788,7 +1863,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + try { +- this.isUpdatingNavigations = true; ++ //this.isUpdatingNavigations = true; // Folia - region threading + iterator = list.iterator(); + + while (iterator.hasNext()) { +@@ -1797,7 +1872,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + navigationabstract1.recomputePath(); + } + } finally { +- this.isUpdatingNavigations = false; ++ //this.isUpdatingNavigations = false; // Folia - region threading + } + + } +@@ -1806,29 +1881,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public void updateNeighborsAt(BlockPos pos, Block block) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement ++ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading + this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, (Direction) null, (Direction) null)); + } + + @Override + public void updateNeighborsAt(BlockPos pos, Block sourceBlock, @Nullable Orientation orientation) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null, orientation); ++ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null, orientation); // Folia - region threading + } + + @Override + public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction, @Nullable Orientation orientation) { +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction, orientation); ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction, orientation); // Folia - region threading + } + + @Override + public void neighborChanged(BlockPos pos, Block sourceBlock, @Nullable Orientation orientation) { +- this.neighborUpdater.neighborChanged(pos, sourceBlock, orientation); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, sourceBlock, orientation); // Folia - region threading + } + + @Override + public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, @Nullable Orientation orientation, boolean notify) { +- this.neighborUpdater.neighborChanged(state, pos, sourceBlock, orientation, notify); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, sourceBlock, orientation, notify); // Folia - region threading + } + + @Override +@@ -1898,7 +1973,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // CraftBukkit end + ParticleOptions particleparam2 = serverexplosion.isSmall() ? particleparam : particleparam1; +- Iterator iterator = this.players.iterator(); ++ Iterator iterator = this.getLocalPlayers().iterator(); // Folia - region thraeding + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +@@ -1919,25 +1994,28 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public void blockEvent(BlockPos pos, Block block, int type, int data) { +- this.blockEvents.add(new BlockEventData(pos, block, type, data)); ++ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, type, data)); // Folia - regionised ticking + } + + private void runBlockEvents() { +- this.blockEventsToReschedule.clear(); ++ List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking + +- while (!this.blockEvents.isEmpty()) { +- BlockEventData blockactiondata = (BlockEventData) this.blockEvents.removeFirst(); ++ // Folia start - regionised ticking ++ io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); ++ BlockEventData blockactiondata; ++ while ((blockactiondata = worldRegionData.removeFirstBlockEvent()) != null) { ++ // Folia end - regionised ticking + + if (this.shouldTickBlocksAt(blockactiondata.pos())) { + if (this.doBlockEvent(blockactiondata)) { + this.server.getPlayerList().broadcast((Player) null, (double) blockactiondata.pos().getX(), (double) blockactiondata.pos().getY(), (double) blockactiondata.pos().getZ(), 64.0D, this.dimension(), new ClientboundBlockEventPacket(blockactiondata.pos(), blockactiondata.block(), blockactiondata.paramA(), blockactiondata.paramB())); + } + } else { +- this.blockEventsToReschedule.add(blockactiondata); ++ blockEventsToReschedule.add(blockactiondata); // Folia - regionised ticking + } + } + +- this.blockEvents.addAll(this.blockEventsToReschedule); ++ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking + } + + private boolean doBlockEvent(BlockEventData event) { +@@ -1948,12 +2026,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public LevelTicks getBlockTicks() { +- return this.blockTicks; ++ return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking + } + + @Override + public LevelTicks getFluidTicks() { +- return this.fluidTicks; ++ return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking + } + + @Nonnull +@@ -1981,7 +2059,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // CraftBukkit start - visibility api support + public int sendParticlesSource(ServerPlayer sender, T t0, boolean flag, boolean flag1, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6) { + // Paper start - Particle API +- return this.sendParticlesSource(this.players, sender, t0, flag, flag1, d0, d1, d2, i, d3, d4, d5, d6); ++ return this.sendParticlesSource(this.getLocalPlayers(), sender, t0, flag, flag1, d0, d1, d2, i, d3, d4, d5, d6); // Folia - region threading + } + public int sendParticlesSource(List receivers, @Nullable ServerPlayer sender, T t0, boolean flag, boolean flag1, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6) { + // Paper end - Particle API +@@ -2039,7 +2117,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public Entity getEntityOrPart(int id) { + Entity entity = (Entity) this.getEntities().get(id); + +- return entity != null ? entity : (Entity) this.dragonParts.get(id); ++ // Folia start - region threading ++ if (entity != null) { ++ return entity; ++ } ++ synchronized (this.dragonParts) { ++ return this.dragonParts.get(id); ++ } ++ // Folia end - region threading + } + + @Override +@@ -2094,6 +2179,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper start - Call missing map initialize event and set id + final DimensionDataStorage storage = this.getServer().overworld().getDataStorage(); + ++ synchronized (storage.cache) { // Folia - region threading + final Optional cacheEntry = storage.cache.get(id.key()); + if (cacheEntry == null) { // Cache did not contain, try to load and may init + final MapItemSavedData worldmap = storage.get(MapItemSavedData.factory(), id.key()); // get populates the cache +@@ -2113,6 +2199,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + return null; ++ } // Folia - region threading + // Paper end - Call missing map initialize event and set id + } + +@@ -2170,6 +2257,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + public boolean setChunkForced(int x, int z, boolean forced) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading + ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks"); + ChunkPos chunkcoordintpair = new ChunkPos(x, z); + long k = chunkcoordintpair.toLong(); +@@ -2178,7 +2266,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + if (forced) { + flag1 = forcedchunk.getChunks().add(k); + if (flag1) { +- this.getChunk(x, z); ++ //this.getChunk(x, z); // Folia - region threading - we must let the chunk load asynchronously + } + } else { + flag1 = forcedchunk.getChunks().remove(k); +@@ -2206,13 +2294,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + BlockPos blockposition1 = pos.immutable(); + + optional.ifPresent((holder) -> { +- this.getServer().execute(() -> { ++ Runnable run = () -> { // Folia - region threading + this.getPoiManager().remove(blockposition1); + DebugPackets.sendPoiRemovedPacket(this, blockposition1); +- }); ++ }; // Folia - region threading ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run ++ ); ++ // Folia end - region threading + }); + optional1.ifPresent((holder) -> { +- this.getServer().execute(() -> { ++ Runnable run = () -> { // Folia - region threading + // Paper start - Remove stale POIs + if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) { + this.getPoiManager().remove(blockposition1); +@@ -2220,7 +2313,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper end - Remove stale POIs + this.getPoiManager().add(blockposition1, holder); + DebugPackets.sendPoiAddedPacket(this, blockposition1); +- }); ++ }; // Folia - region threading ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run ++ ); ++ // Folia end - region threading + }); + } + } +@@ -2267,7 +2365,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + BufferedWriter bufferedwriter = Files.newBufferedWriter(path.resolve("stats.txt")); + + try { +- bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); ++ //bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); // Folia - region threading + NaturalSpawner.SpawnState spawnercreature_d = this.getChunkSource().getLastSpawnState(); + + if (spawnercreature_d != null) { +@@ -2281,7 +2379,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system +- bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); ++ //bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading + bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); + bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); + bufferedwriter.write("distance_manager: " + playerchunkmap.getDistanceManager().getDebugStatus() + "\n"); +@@ -2427,7 +2525,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + private void dumpBlockEntityTickers(Writer writer) throws IOException { + CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(writer); +- Iterator iterator = this.blockEntityTickers.iterator(); ++ Iterator iterator = null; // Folia - region threading + + while (iterator.hasNext()) { + TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next(); +@@ -2440,7 +2538,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @VisibleForTesting + public void clearBlockEvents(BoundingBox box) { +- this.blockEvents.removeIf((blockactiondata) -> { ++ this.getCurrentWorldData().removeIfBlockEvents((blockactiondata) -> { // Folia - regionised ticking + return box.isInside(blockactiondata.pos()); + }); + } +@@ -2449,7 +2547,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public void blockUpdated(BlockPos pos, Block block) { + if (!this.isDebug()) { + // CraftBukkit start +- if (this.populating) { ++ if (this.getCurrentWorldData().populating) { // Folia - region threading + return; + } + // CraftBukkit end +@@ -2492,9 +2590,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @VisibleForTesting + public String getWatchdogStats() { +- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system +- return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); +- }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); ++ return "region threading"; // Folia - region threading + } + + private static String getTypeCount(Iterable items, Function classifier) { +@@ -2544,17 +2640,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + public void startTickingChunk(LevelChunk chunk) { +- chunk.unpackTicks(this.getLevelData().getGameTime()); ++ chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading + } + + public void onStructureStartsAvailable(ChunkAccess chunk) { +- this.server.execute(() -> { +- this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); +- }); ++ // Folia start - region threading ++ // no longer needs to be on main ++ this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); ++ // Folia end - region threading + } + + public PathTypeCache getPathTypeCache() { +- return this.pathTypesByPosCache; ++ return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading + } + + @Override +@@ -2574,7 +2671,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system + } + +- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { ++ public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public + // Paper start - rewrite chunk system + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); + // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded +@@ -2669,7 +2766,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Paper start - optimize redstone (Alternate Current) + @Override + public alternate.current.wire.WireHandler getWireHandler() { +- return wireHandler; ++ return this.getCurrentWorldData().wireHandler; // Folia - region threading + } + // Paper end - optimize redstone (Alternate Current) + +@@ -2680,16 +2777,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public void onCreated(Entity entity) {} + + public void onDestroyed(Entity entity) { +- ServerLevel.this.getScoreboard().entityRemoved(entity); ++ // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading + } + + public void onTickingStart(Entity entity) { + if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking +- ServerLevel.this.entityTickList.add(entity); ++ ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading + } + + public void onTickingEnd(Entity entity) { +- ServerLevel.this.entityTickList.remove(entity); ++ ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading + // Paper start - Reset pearls when they stop being ticked + if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { + pearl.cachedOwner = null; +@@ -2700,6 +2797,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public void onTrackingStart(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot ++ ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading + // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true + if (entity instanceof ServerPlayer entityplayer) { + ServerLevel.this.players.add(entityplayer); +@@ -2713,7 +2811,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); + } + +- ServerLevel.this.navigatingMobs.add(entityinsentient); ++ ServerLevel.this.getCurrentWorldData().addNavigatingMob(entityinsentient); // Folia - region threading + } + + if (entity instanceof EnderDragon entityenderdragon) { +@@ -2723,7 +2821,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + for (int j = 0; j < i; ++j) { + EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + ++ synchronized (ServerLevel.this.dragonParts) { // Folia - region threading + ServerLevel.this.dragonParts.put(entitycomplexpart.getId(), entitycomplexpart); ++ } // Folia - region threading + } + } + +@@ -2745,16 +2845,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public void onTrackingEnd(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot ++ ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); + // Spigot start + if ( entity instanceof Player ) + { + com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> + { +- for (Object o : worldData.cache.values() ) ++ // Folia start - make map data thread-safe ++ List worldDataCache; ++ synchronized (worldData.cache) { ++ worldDataCache = new java.util.ArrayList<>(worldData.cache.values()); ++ } ++ for (Object o : worldDataCache ) ++ // Folia end - make map data thread-safe + { + if ( o instanceof MapItemSavedData ) + { + MapItemSavedData map = (MapItemSavedData) o; ++ synchronized (map) { // Folia - make map data thread-safe + map.carriedByPlayers.remove( (Player) entity ); + for ( Iterator iter = (Iterator) map.carriedBy.iterator(); iter.hasNext(); ) + { +@@ -2764,6 +2872,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + iter.remove(); + } + } ++ } // Folia - make map data thread-safe + } + } + } ); +@@ -2794,7 +2903,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); + } + +- ServerLevel.this.navigatingMobs.remove(entityinsentient); ++ ServerLevel.this.getCurrentWorldData().removeNavigatingMob(entityinsentient); // Folia - region threading + } + + if (entity instanceof EnderDragon entityenderdragon) { +@@ -2804,13 +2913,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + for (int j = 0; j < i; ++j) { + EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + ++ synchronized (ServerLevel.this.dragonParts) { // Folia - region threading + ServerLevel.this.dragonParts.remove(entitycomplexpart.getId()); ++ } // Folia - region threading + } + } + + entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); + // CraftBukkit start + entity.valid = false; ++ // Folia - region threading - TODO THIS SHIT + if (!(entity instanceof ServerPlayer)) { + for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players + player.getBukkitEntity().onEntityRemove(entity); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index fc7f7a34babd095a51b5321f600aef65a2a9d123..130643b97fdab3bf89fc87afd6d4e0b922dac538 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -220,7 +220,7 @@ import org.bukkit.inventory.MainHand; + public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); +- public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving ++ public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // Paper // Folia - threaded regions - changed to nanoTime + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + private static final int FLY_STAT_RECORDING_SPEED = 25; +@@ -541,8 +541,149 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + } + // CraftBukkit end + ++ // Folia start - region threading ++ private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5; ++ ++ private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) { ++ BlockPos spawn = world.getSharedSpawnPos(); ++ double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS)); ++ ++ double spawnX = (double)spawn.getX() + 0.5; ++ double spawnZ = (double)spawn.getZ() + 0.5; ++ ++ net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); ++ ++ double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius); ++ double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius); ++ double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius); ++ double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius); ++ ++ double amountX = selectMaxX - selectMinX; ++ double amountZ = selectMaxZ - selectMinZ; ++ ++ int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); ++ int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); ++ ++ return new BlockPos(selectX, 0, selectZ); ++ } ++ ++ private static void completeSpawn(ServerLevel world, BlockPos selected, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(selected), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ++ private static BlockPos findSpawnAround(ServerLevel world, ServerPlayer player, BlockPos selected) { ++ // try hard to find, so that we don't attempt another chunk load ++ for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) { ++ for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) { ++ BlockPos inChunk = PlayerRespawnLogic.getOverworldRespawnPos(world, selected.getX() + dx, selected.getZ() + dz); ++ if (inChunk == null) { ++ continue; ++ } ++ ++ AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); ++ ++ if (!player.noCollisionNoLiquid(world, checkVolume)) { ++ continue; ++ } ++ ++ return inChunk; ++ } ++ } ++ ++ return null; ++ } ++ ++ // rets false when another attempt is required ++ private static boolean trySpawnOrSchedule(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ ++attemptCount[0]; ++ ++ BlockPos rough = getRandomSpawn(world, random); ++ ++ // add 2 to ensure that the chunks are loaded for collision checks ++ int minX = (rough.getX() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int minZ = (rough.getZ() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int maxX = (rough.getX() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int maxZ = (rough.getZ() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ ++ // we could short circuit this check, but it would possibly recurse. Then, it could end up causing a stack overflow ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, minX, minZ, maxX, maxZ) || !world.moonrise$areChunksLoaded(minX, minZ, maxX, maxZ)) { ++ world.moonrise$loadChunksAsync(minX, maxX, minZ, maxZ, ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (unused) -> { ++ BlockPos selected = findSpawnAround(world, player, rough); ++ if (selected == null) { ++ // run more spawn attempts ++ selectSpawn(world, player, random, attemptCount, maxAttempts, toComplete); ++ return; ++ } ++ ++ completeSpawn(world, selected, toComplete); ++ return; ++ } ++ ); ++ return true; ++ } ++ ++ BlockPos selected = findSpawnAround(world, player, rough); ++ if (selected == null) { ++ return false; ++ } ++ ++ completeSpawn(world, selected, toComplete); ++ return true; ++ } ++ ++ private static void selectSpawn(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ do { ++ if (attemptCount[0] >= maxAttempts) { ++ BlockPos sharedSpawn = world.getSharedSpawnPos(); ++ ++ LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius"); ++ ++ selectSpawnWithoutRadius(world, player, sharedSpawn, toComplete); ++ return; ++ } ++ } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete)); ++ } ++ ++ ++ private static void selectSpawnWithoutRadius(ServerLevel world, ServerPlayer player, BlockPos spawn, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawn.getX() + 0.5, spawn.getY(), spawn.getZ() + 0.5), ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (c) -> { ++ BlockPos ret = spawn; ++ while (!player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5)) && ret.getY() < (double)world.getMaxY()) { ++ ret = ret.above(); ++ } ++ while (player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY() - 1, ret.getZ() + 0.5)) && ret.getY() > (double)(world.getMinY() + 1)) { ++ ret = ret.below(); ++ } ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ); ++ } ++ ++ public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading ++ BlockPos blockposition = world.getSharedSpawnPos(); ++ ++ if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit ++ selectSpawn(world, player, player.random, new int[1], 500, toComplete); ++ } else { ++ selectSpawnWithoutRadius(world, player, blockposition, toComplete); ++ } ++ ++ } ++ // Folia end - region threading ++ + @Override + public BlockPos adjustSpawnLocation(ServerLevel world, BlockPos basePos) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ // Folia end - region threading + AABB axisalignedbb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO); + BlockPos blockposition1 = basePos; + +@@ -883,11 +1024,18 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + + if (worldserver != null) { + Entity entity = EntityType.loadEntityRecursive(nbttagcompound, worldserver, EntitySpawnReason.LOAD, (entity1) -> { +- return !worldserver.addWithUUID(entity1) ? null : entity1; ++ return entity1; // Folia - region threading - delay world add + }); + + if (entity != null) { +- ServerPlayer.placeEnderPearlTicket(worldserver, entity.chunkPosition()); ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> { ++ worldserver.addFreshEntityWithPassengers(entity); ++ ServerPlayer.placeEnderPearlTicket(worldserver, entity.chunkPosition()); ++ } ++ ); ++ // Folia end - region threading + } else { + ServerPlayer.LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional1.get()); + } +@@ -1572,6 +1720,324 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + + } + ++ // Folia start - region threading ++ /** ++ * Teleport flag indicating that the player is to be respawned, expected to only be used ++ * internally for {@link #respawn(java.util.function.Consumer, PlayerRespawnEvent.RespawnReason)} ++ */ ++ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; ++ ++ public void exitEndCredits() { ++ if (!this.wonGame) { ++ // not in the end credits anymore ++ return; ++ } ++ this.wonGame = false; ++ ++ this.respawn((player) -> { ++ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); ++ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL, true); ++ } ++ ++ public void respawn(java.util.function.Consumer respawnComplete, PlayerRespawnEvent.RespawnReason reason) { ++ this.respawn(respawnComplete, reason, false); ++ } ++ ++ private void respawn(java.util.function.Consumer respawnComplete, PlayerRespawnEvent.RespawnReason reason, boolean alive) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); ++ ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ ++ if (alive != this.isAlive()) { ++ throw new IllegalStateException("isAlive expected = " + alive); ++ } ++ ++ if (!this.hasNullCallback()) { ++ this.unRide(); ++ } ++ ++ if (this.isVehicle() || this.isPassenger()) { ++ throw new IllegalStateException("Dead player should not be a vehicle or passenger"); ++ } ++ ++ ServerLevel origin = this.serverLevel(); ++ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); ++ ++ // modified based off PlayerList#respawn ++ ++ EntityTreeNode passengerTree = this.makePassengerTree(); ++ ++ this.isChangingDimension = true; ++ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); ++ // reset player if needed, only after removal from world ++ if (!alive) { ++ ServerPlayer.this.reset(); ++ } ++ // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks ++ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ ++ BlockPos respawnPos = this.getRespawnPosition(); ++ float respawnAngle = this.getRespawnAngle(); ++ boolean isRespawnForced = this.isRespawnForced(); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable spawnPosComplete = ++ new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ boolean[] usedRespawnAnchor = new boolean[1]; ++ ++ // set up post spawn location logic ++ spawnPosComplete.addWaiter((spawnLoc, throwable) -> { ++ // update pos and velocity ++ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); ++ ServerPlayer.this.setYRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setXRot(spawnLoc.getPitch()); ++ ServerPlayer.this.setDeltaMovement(Vec3.ZERO); ++ // placeInAsync will update the world ++ ++ this.placeInAsync( ++ origin, ++ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks ++ // stay loaded for a bit with the teleport ticket ++ ((CraftWorld)spawnLoc.getWorld()).getHandle(), ++ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN, ++ passengerTree, // note: we expect this to just be the player, no passengers ++ (entity) -> { ++ // now the player is in the world, and can receive sound ++ if (usedRespawnAnchor[0]) { ++ ServerPlayer.this.connection.send( ++ new ClientboundSoundPacket( ++ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, ++ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), ++ 1.0F, 1.0F, ServerPlayer.this.serverLevel().getRandom().nextLong() ++ ) ++ ); ++ } ++ // now the respawn logic is complete ++ ++ // last, call the function callback ++ if (respawnComplete != null) { ++ respawnComplete.accept(ServerPlayer.this); ++ } ++ } ++ ); ++ }); ++ ++ // find and modify respawn block state ++ if (respawnWorld == null || respawnPos == null) { ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ } else { ++ // load chunk for block ++ // give at least 1 radius of loaded chunks so that we do not sync load anything ++ int radiusBlocks = 16; ++ respawnWorld.moonrise$loadChunksAsync(respawnPos, radiusBlocks, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (chunks) -> { ++ ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock( ++ respawnWorld, respawnPos, respawnAngle, isRespawnForced, !alive ++ ).orElse(null); ++ if (spawnPos == null) { ++ // no spawn ++ ServerPlayer.this.connection.send( ++ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) ++ ); ++ ServerPlayer.this.setRespawnPosition( ++ null, null, 0f, false, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ return; ++ } ++ ++ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR); ++ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); ++ usedRespawnAnchor[0] = !alive && isRespawnAnchor; ++ ++ ServerPlayer.this.setRespawnPosition( ++ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ ++ // finished now, pass the location on ++ spawnPosComplete.complete( ++ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos.position(), spawnPos.yaw(), 0.0f) ++ ); ++ return; ++ } ++ ); ++ } ++ } ++ ++ @Override ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ pos, this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ this.setOldPosAndRot(); ++ this.resetStoredPositions(); ++ } ++ ++ @Override ++ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ // must be manually removed from connections ++ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); ++ ++ this.spawnIn(destination); ++ this.transform(pos, yaw, pitch, velocity); ++ ++ return this; ++ } ++ ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopUsingItem(); ++ } ++ ++ @Override ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ ++ this.postChangeDimension(); ++ } else { ++ // Modelled after PlayerList#respawn ++ ++ // We avoid checking for disconnection here, which means we do not have to add/remove from ++ // the player list here. We can let this be properly handled by the connection handler ++ ++ // pre-add logic ++ PlayerList playerlist = this.server.getPlayerList(); ++ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); ++ this.connection.send( ++ new ClientboundRespawnPacket( ++ this.createCommonSpawnInfo(destination), ++ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0 ++ ) ++ ); ++ // don't bother with the chunk cache radius and simulation distance packets, they are handled ++ // by the chunk loader ++ this.spawnIn(destination); // important that destination != null ++ // we can delay teleport until later, the player position is already set up at the target ++ this.setShiftKeyDown(false); ++ ++ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( ++ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() ++ )); ++ this.connection.send(new ClientboundChangeDifficultyPacket( ++ worlddata.getDifficulty(), worlddata.isDifficultyLocked() ++ )); ++ this.connection.send(new ClientboundSetExperiencePacket( ++ this.experienceProgress, this.totalExperience, this.experienceLevel ++ )); ++ ++ playerlist.sendActivePlayerEffects(this); ++ playerlist.sendLevelInfo(this, destination); ++ playerlist.sendPlayerPermissionLevel(this); ++ ++ // regular world add logic ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ ++ // delay callback until after post add logic ++ ++ // post add logic ++ ++ // "Added from changeDimension" ++ this.setHealth(this.getHealth()); ++ playerlist.sendAllPlayerInfo(this); ++ this.onUpdateAbilities(); ++ /*for (MobEffectInstance mobEffect : this.getActiveEffects()) { ++ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect, false)); ++ }*/ // handled by sendActivePlayerEffects ++ ++ // Paper start - Reset shield blocking on dimension change ++ if (this.isBlocking()) { ++ this.stopUsingItem(); ++ } ++ // Paper end - Reset shield blocking on dimension change ++ ++ this.triggerDimensionChangeTriggers(originWorld); ++ ++ // finished ++ ++ this.postChangeDimension(); ++ } ++ } ++ ++ @Override ++ public boolean endPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ if (this.level().getTypeKey() == LevelStem.END) { ++ if (!this.canPortalAsync(null, false)) { ++ return false; ++ } ++ this.wonGame = true; ++ // TODO is there a better solution to this that DOESN'T skip the credits? ++ this.seenCredits = true; ++ if (!this.seenCredits) { ++ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F)); ++ } ++ this.exitEndCredits(); ++ return true; ++ } else { ++ return super.endPortalLogicAsync(portalPos); ++ } ++ } ++ ++ @Override ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ super.prePortalLogic(origin, destination, type); ++ if (origin.getTypeKey() == LevelStem.OVERWORLD && destination.getTypeKey() == LevelStem.NETHER) { ++ this.enteredNetherPosition = this.position(); ++ } ++ } ++ // Folia end - region threading ++ + @Nullable + @Override + public ServerPlayer teleport(TeleportTransition teleportTarget) { +@@ -2605,6 +3071,12 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + public void setCamera(@Nullable Entity entity) { + Entity entity1 = this.getCamera(); + ++ // Folia start - region threading ++ if (entity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ return; ++ } ++ // Folia end - region threading ++ + this.camera = (Entity) (entity == null ? this : entity); + if (entity1 != this.camera) { + // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity +@@ -3098,11 +3570,11 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + } + + public void registerEnderPearl(ThrownEnderpearl enderPearl) { +- this.enderPearls.add(enderPearl); ++ //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls + } + + public void deregisterEnderPearl(ThrownEnderpearl enderPearl) { +- this.enderPearls.remove(enderPearl); ++ //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls + } + + public Set getEnderPearls() { +@@ -3261,7 +3733,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + this.experienceLevel = this.newLevel; + this.totalExperience = this.newTotalExp; + this.experienceProgress = 0; +- this.deathTime = 0; ++ this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading + this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent + this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); + this.effectsDirty = true; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index a96f859a5d0c6ec692d4627a69f3c9ee49199dbc..ad8da6726b4113068b200426ab1ac1e24a061942 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -131,7 +131,7 @@ public class ServerPlayerGameMode { + BlockState iblockdata; + + if (this.hasDelayedDestroy) { +- iblockdata = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks ++ iblockdata = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks // Folia - region threading - don't destroy blocks not owned + if (iblockdata == null || iblockdata.isAir()) { // Paper - Don't allow digging into unloaded chunks + this.hasDelayedDestroy = false; + } else { +@@ -144,7 +144,7 @@ public class ServerPlayerGameMode { + } + } else if (this.isDestroyingBlock) { + // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead +- iblockdata = this.level.getBlockStateIfLoaded(this.destroyPos); ++ iblockdata = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); + if (iblockdata == null) { + this.isDestroyingBlock = false; + return; +@@ -422,7 +422,7 @@ public class ServerPlayerGameMode { + } else { + // CraftBukkit start + org.bukkit.block.BlockState state = bblock.getState(); +- this.level.captureDrops = new ArrayList<>(); ++ this.level.getCurrentWorldData().captureDrops = new ArrayList<>(); // Folia - region threading + // CraftBukkit end + BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player); + boolean flag = this.level.removeBlock(pos, false); +@@ -450,8 +450,8 @@ public class ServerPlayerGameMode { + // return true; // CraftBukkit + } + // CraftBukkit start +- java.util.List itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world +- this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff ++ java.util.List itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - capture all item additions to the world // Folia - region threading ++ this.level.getCurrentWorldData().captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff // Folia - region threading + if (event.isDropItems()) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world + } +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 4d3f7d4a8e05a8d84aa5202134eda1ce9621712e..02e97634932f4aea6ea36a8e6479ec90cbba0a54 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -26,6 +26,14 @@ public class TicketType { + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + public static final TicketType POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type ++ // Folia start - region threading ++ public static final TicketType LOGIN = create("folia:login", (u1, u2) -> 0, 20); ++ public static final TicketType DELAYED = create("folia:delay", (u1, u2) -> 0, 5); ++ public static final TicketType END_GATEWAY_EXIT_SEARCH = create("folia:end_gateway_exit_search", Long::compareTo); ++ public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("folia:nether_portal_double_check", Long::compareTo); ++ public static final TicketType TELEPORT_HOLD_TICKET = create("folia:teleport_hold_ticket", Long::compareTo); ++ public static final TicketType REGION_SCHEDULER_API_HOLD = create("folia:region_scheduler_api_hold", (a, b) -> 0); ++ // Folia end - region threading + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index e4b0dc3121101d54394a0c3a413dabf8103b2ea6..0026ec8a393d4348d36242d745e4eccf6327744c 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -115,6 +115,14 @@ public class WorldGenRegion implements WorldGenLevel { + } + // Paper end - rewrite chunk system + ++ // Folia start - region threading ++ private final net.minecraft.world.level.StructureManager structureManager; ++ @Override ++ public net.minecraft.world.level.StructureManager structureManager() { ++ return this.structureManager; ++ } ++ // Folia end - region threading ++ + public WorldGenRegion(ServerLevel world, StaticCache2D chunks, ChunkStep generationStep, ChunkAccess centerPos) { + this.generatingStep = generationStep; + this.cache = chunks; +@@ -125,6 +133,7 @@ public class WorldGenRegion implements WorldGenLevel { + this.random = world.getChunkSource().randomState().getOrCreateRandomFactory(WorldGenRegion.WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition()); + this.dimensionType = world.dimensionType(); + this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed)); ++ this.structureManager = world.structureManager().forWorldGenRegion(this); // Folia - region threading + } + + public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { +diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index b0bc66dc7248aae691dcab68b925b52a1695e63f..d01063b964a67ecff2998a9e02e7f37a9af88c84 100644 +--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -114,6 +114,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + } + ++ // Folia start - region threading ++ private boolean handledDisconnect = false; ++ // Folia end - region threading ++ + @Override + public void onDisconnect(DisconnectionDetails info) { + // Paper start - Fix kick event leave message not being sent +@@ -121,10 +125,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) { + // Paper end - Fix kick event leave message not being sent ++ // Folia start - region threading ++ if (this.handledDisconnect) { ++ // avoid retiring scheduler twice ++ return; ++ } ++ this.handledDisconnect = true; ++ // Folia end - region threading + if (this.isSingleplayerOwner()) { + ServerCommonPacketListenerImpl.LOGGER.info("Stopping singleplayer server as player logged out"); + this.server.halt(false); + } ++ this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading + + } + +@@ -354,24 +366,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + if (this.processedDisconnect) { + return; + } +- if (!this.cserver.isPrimaryThread()) { +- Waitable waitable = new Waitable() { +- @Override +- protected Object evaluate() { +- ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); // Paper - kick event causes +- return null; +- } +- }; +- +- this.server.processQueue.add(waitable); +- +- try { +- waitable.get(); +- } catch (InterruptedException e) { +- Thread.currentThread().interrupt(); +- } catch (ExecutionException e) { +- throw new RuntimeException(e); +- } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading ++ this.connection.disconnectSafely(disconnectionInfo.reason(), cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region + return; + } + +@@ -404,7 +400,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + Objects.requireNonNull(this.connection); + // CraftBukkit - Don't wait +- minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper ++ // Folia - region threading + } + + // Paper start - add proper async disconnect +@@ -417,18 +413,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + + public void disconnectAsync(DisconnectionDetails disconnectionInfo, PlayerKickEvent.Cause cause) { +- if (this.cserver.isPrimaryThread()) { +- this.disconnect(disconnectionInfo, cause); +- return; +- } +- this.connection.setReadOnly(); +- this.server.scheduleOnMain(() -> { +- ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); +- if (ServerCommonPacketListenerImpl.this.player.quitReason == null) { +- // cancelled +- ServerCommonPacketListenerImpl.this.connection.enableAutoRead(); +- } +- }); ++ this.disconnect(disconnectionInfo, cause); // Folia - threaded regions + } + // Paper end - add proper async disconnect + +diff --git a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index 880e5c52746e9e3a9a1f42ec6461be54e3ee136c..70665e5bea075ced6db7696efcffe502f81cffdf 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -54,6 +54,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + private ClientInformation clientInformation; + @Nullable + private SynchronizeRegistriesTask synchronizeRegistriesTask; ++ public boolean switchToMain = false; // Folia - region threading - rewrite login process + + // CraftBukkit start + public ServerConfigurationPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { +@@ -170,7 +171,58 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + + ServerPlayer entityplayer = playerlist.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit + +- playerlist.placeNewPlayer(this.connection, entityplayer, this.createCookie(this.clientInformation)); ++ // Folia start - region threading - rewrite login process ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread"); ++ CommonListenerCookie clientData = this.createCookie(this.clientInformation); ++ org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously ++ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, ++ // on the load callback for those chunks (so on the same region) ++ // this guarantees the chunk cannot unload under our feet ++ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { ++ int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4; ++ int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4; ++ ++ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); ++ // we just need to hold the chunks at loaded until the next tick ++ // so we do not need to care about unique IDs for the ticket ++ world.getChunkSource().addTicketAtLevel( ++ net.minecraft.server.level.TicketType.LOGIN, ++ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // once switchToMain is set, the current ticking region now owns the connection and is responsible ++ // for cleaning it up ++ playerlist.placeNewPlayer( ++ ServerConfigurationPacketListenerImpl.this.connection, ++ entityplayer, ++ clientData, ++ java.util.Optional.ofNullable(data.getValue()), ++ lastKnownName.getValue(), ++ loc ++ ); ++ }, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER ++ ); ++ }); ++ this.switchToMain = true; ++ try { ++ // now the connection responsibility is transferred on the region ++ playerlist.loadSpawnForNewPlayer(this.connection, entityplayer, clientData, data, lastKnownName, toComplete); ++ } catch (final Throwable throwable) { ++ // assume toComplete will not be invoked ++ // ensure global tick thread owns the connection again, to properly disconnect it ++ this.switchToMain = false; ++ throw new RuntimeException(throwable); ++ } ++ // Folia end - region threading - rewrite login process + } catch (Exception exception) { + ServerConfigurationPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception); + // Paper start - Debugging +diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +index 3a9e25b436f366fffe08c3b0c1fce11ed42ee646..ae88c6e2635b1608383f8c74813d723f7c6ffaf8 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java ++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +@@ -167,10 +167,13 @@ public class ServerConnectionListener { + }); + } + // Paper end - Add support for proxy protocol +- pending.add(object); // Paper - prevent blocking on adding a new connection while the server is ticking ++ // Folia - connection fixes - move down + ((Connection) object).configurePacketHandler(channelpipeline); + ((Connection) object).setListenerForServerboundHandshake(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object)); + io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners ++ // Folia start - regionised threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(object); ++ // Folia end - regionised threading + } + }).group(eventloopgroup).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper - Unix domain socket support + } +@@ -232,7 +235,7 @@ public class ServerConnectionListener { + // Spigot Start + this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking + // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order +- if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 ) ++ if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0 ) // Folia - region threading + { + Collections.shuffle( this.connections ); + } +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 84fa24880d02dc7ba1ec8bda3575be38447fd4b2..7783e023e84ab84e3903fc0371aad08a82cf69d0 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -312,7 +312,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20); + private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault(); + private final FutureChain chatMessageChain; +- private boolean waitingForSwitchToConfig; ++ public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public + private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length + + public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie clientData) { +@@ -329,10 +329,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + // CraftBukkit start - add fields and methods +- private int lastTick = MinecraftServer.currentTick; ++ private long lastTick = Util.getMillis() / 50L; // Folia - region threading + private int allowedPlayerTicks = 1; +- private int lastDropTick = MinecraftServer.currentTick; +- private int lastBookTick = MinecraftServer.currentTick; ++ private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading ++ private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading + private int dropCount = 0; + + private boolean hasMoved = false; +@@ -344,8 +344,21 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + private boolean justTeleported = false; + // CraftBukkit end + ++ // Folia start - region threading ++ public net.minecraft.world.level.ChunkPos disconnectPos; ++ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); ++ public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); ++ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); ++ // Folia end - region threading ++ + @Override + public void tick() { ++ // Folia start - region threading ++ this.keepConnectionAlive(); ++ if (this.processedDisconnect || this.player.wonGame) { ++ return; ++ } ++ // Folia end - region threading + if (this.ackBlockChangesUpTo > -1) { + this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); + this.ackBlockChangesUpTo = -1; +@@ -394,7 +407,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.aboveGroundVehicleTickCount = 0; + } + +- this.keepConnectionAlive(); ++ // Folia - region threading - moved to beginning of method + this.chatSpamThrottler.tick(); + this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits + this.recipeSpamPackets.tick(); // Paper - auto recipe limit +@@ -432,6 +445,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.lastGoodX = this.player.getX(); + this.lastGoodY = this.player.getY(); + this.lastGoodZ = this.player.getZ(); ++ // Folia start - support vehicle teleportations ++ this.lastVehicle = this.player.getRootVehicle(); ++ if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) { ++ this.vehicleFirstGoodX = this.lastVehicle.getX(); ++ this.vehicleFirstGoodY = this.lastVehicle.getY(); ++ this.vehicleFirstGoodZ = this.lastVehicle.getZ(); ++ this.vehicleLastGoodX = this.lastVehicle.getX(); ++ this.vehicleLastGoodY = this.lastVehicle.getY(); ++ this.vehicleLastGoodZ = this.lastVehicle.getZ(); ++ } else { ++ this.lastVehicle = null; ++ } ++ // Folia end - support vehicle teleportations + } + + @Override +@@ -538,9 +564,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // Paper end - fix large move vectors killing the server + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Folia - region threading + + ++this.receivedMovePacketCount; + int i = this.receivedMovePacketCount - this.knownMovePacketCount; +@@ -614,7 +641,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + entity.absMoveTo(d3, d4, d5, f, f1); +- this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit ++ //this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers + + // Paper start - optimise out extra getCubes + boolean teleportBack = flag2; // violating this is always a fail +@@ -627,11 +654,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + if (teleportBack) { // Paper end - optimise out extra getCubes + entity.absMoveTo(d0, d1, d2, f, f1); +- this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit ++ //this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated + this.send(ClientboundMoveVehiclePacket.fromEntity(entity)); + return; + } + ++ // Folia start - move to positionRider ++ // this correction is required on folia since we move the connection tick to the beginning of the server ++ // tick, which would make any desync here visible ++ // this will correctly update the passenger positions for all mounted entities ++ // this prevents desync and ensures that all passengers have the correct rider-adjusted position ++ entity.repositionAllPassengers(false); ++ // Folia end - move to positionRider ++ + // CraftBukkit start - fire PlayerMoveEvent + Player player = this.getCraftPlayer(); + if (!this.hasMoved) { +@@ -662,7 +697,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +- this.teleport(from); ++ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -670,7 +705,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. + // We only do this if the Event was not cancelled. + if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { +- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); ++ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -838,7 +873,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + // This needs to be on main +- this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringreader)); ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> this.sendServerSuggestions(packet, stringreader), null, 1L); // Folia - region threading + } else if (!completions.isEmpty()) { + final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringreader.getTotalLength()); + final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1); +@@ -1265,11 +1300,11 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + // Paper end - Book size limits + // CraftBukkit start +- if (this.lastBookTick + 20 > MinecraftServer.currentTick) { ++ if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading + this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect + return; + } +- this.lastBookTick = MinecraftServer.currentTick; ++ this.lastBookTick = this.lastTick; // Folia - region threading + // CraftBukkit end + int i = packet.slot(); + +@@ -1286,7 +1321,22 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.updateBookContents(list1, i); + }; + +- this.filterTextPacket((List) list).thenAcceptAsync(consumer, this.server); ++ // Folia start - region threading ++ this.filterTextPacket(list).thenAcceptAsync( ++ consumer, ++ (Runnable run) -> { ++ this.player.getBukkitEntity().taskScheduler.schedule( ++ (player) -> { ++ run.run(); ++ }, ++ null, 1L); ++ } ++ ).whenComplete((Object res, Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.error("Failed to handle book update packet", thr); ++ } ++ }); ++ // Folia end - region threading + } + } + +@@ -1434,9 +1484,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + int i = this.receivedMovePacketCount - this.knownMovePacketCount; + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Folia - region threading + + if (i > Math.max(this.allowedPlayerTicks, 5)) { + ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); +@@ -1628,7 +1679,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +- this.teleport(from); ++ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -1636,7 +1687,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. + // We only do this if the Event was not cancelled. + if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { +- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); ++ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -1883,9 +1934,9 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + if (!this.player.isSpectator()) { + // limit how quickly items can be dropped + // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. +- if (this.lastDropTick != MinecraftServer.currentTick) { ++ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - region threading + this.dropCount = 0; +- this.lastDropTick = MinecraftServer.currentTick; ++ this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading + } else { + // Else we increment the drop count and check the amount. + this.dropCount++; +@@ -1913,7 +1964,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + case ABORT_DESTROY_BLOCK: + case STOP_DESTROY_BLOCK: + // Paper start - Don't allow digging into unloaded chunks +- if (this.player.level().getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned + this.player.connection.ackBlockChangesUpTo(packet.getSequence()); + return; + } +@@ -1998,7 +2049,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // Paper end - improve distance check + BlockPos blockposition = movingobjectpositionblock.getBlockPos(); + +- if (this.player.canInteractWithBlock(blockposition, 1.0D)) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockposition, 1.0D)) { // Folia - do not allow players to interact with blocks outside the current region + Vec3 vec3d1 = vec3d.subtract(Vec3.atCenterOf(blockposition)); + double d0 = 1.0000001D; + +@@ -2132,7 +2183,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + Entity entity = packet.getEntity(worldserver); + + if (entity != null) { +- this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading + return; + } + } +@@ -2167,7 +2218,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + // CraftBukkit end + ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), info.reason().getString()); +- this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent ++ if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent // Folia - region threading + super.onDisconnect(info, quitMessage); // Paper - Fix kick event leave message not being sent + } + +@@ -2176,6 +2227,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.removePlayerFromWorld(null); + } + ++ public boolean hackSwitchingConfig; // Folia - rewrite login process ++ + private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) { + // Paper end - Fix kick event leave message not being sent + this.chatMessageChain.close(); +@@ -2188,6 +2241,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.player.disconnect(); + // Paper start - Adventure + quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used ++ if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player ++ if (!this.hackSwitchingConfig) this.player.serverLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost + if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); + // Paper end +@@ -2444,7 +2499,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + this.player.resetLastActionTime(); + // CraftBukkit start + if (sync) { +- this.server.execute(runnable); ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> runnable.run(), null, 1L); // Folia - region threading + } else { + runnable.run(); + } +@@ -2502,7 +2557,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + String originalFormat = event.getFormat(), originalMessage = event.getMessage(); + this.cserver.getPluginManager().callEvent(event); + +- if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading + // Evil plugins still listening to deprecated event + final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); + queueEvent.setCancelled(event.isCancelled()); +@@ -2600,6 +2655,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + if (s.isEmpty()) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message"); + } else if (this.getCraftPlayer().isConversing()) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + final String conversationInput = s; + this.server.processQueue.add(new Runnable() { + @Override +@@ -2838,8 +2894,25 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + // Spigot End + + public void switchToConfig() { +- this.waitingForSwitchToConfig = true; ++ // Folia start - rewrite login process ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main"); ++ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { ++ throw new IllegalStateException("Cannot switch config while on global tick thread"); ++ } ++ // Folia end - rewrite login process ++ // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world ++ // the field write ordering is bad as it allows the client to send the response packet before the player is ++ // removed from the world ++ // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world ++ try { // Folia - rewrite login process - move connection ownership to global region ++ this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler + this.removePlayerFromWorld(); ++ } finally { // Folia start - rewrite login process - move connection ownership to global region ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData(); ++ worldData.connections.remove(this.connection); ++ // once waitingForSwitchToConfig is set, the global tick thread will own the connection ++ } // Folia end - rewrite login process - move connection ownership to global region ++ this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down + this.send(ClientboundStartConfigurationPacket.INSTANCE); + this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); + } +@@ -2866,7 +2939,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + this.player.resetLastActionTime(); + this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); +- if (entity != null) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) && entity != null) { // Folia - region threading - do not allow interaction of entities outside the current region + if (!worldserver.getWorldBorder().isWithinBounds(entity.blockPosition())) { + return; + } +@@ -3014,6 +3087,12 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + switch (packetplayinclientcommand_enumclientcommand) { + case PERFORM_RESPAWN: + if (this.player.wonGame) { ++ // Folia start - region threading ++ if (true) { ++ this.player.exitEndCredits(); ++ return; ++ } ++ // Folia end - region threading + this.player.wonGame = false; + this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit + this.resetPosition(); +@@ -3023,6 +3102,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + return; + } + ++ // Folia start - region threading ++ if (true) { ++ this.player.respawn((ServerPlayer player) -> { ++ if (ServerGamePacketListenerImpl.this.server.isHardcore()) { ++ ServerGamePacketListenerImpl.this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent ++ ((GameRules.BooleanValue) ServerGamePacketListenerImpl.this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, ServerGamePacketListenerImpl.this.player.serverLevel()); // CraftBukkit - per-world ++ } ++ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); ++ return; ++ } ++ // Folia end - region threading + this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit + this.resetPosition(); + if (this.server.isHardcore()) { +@@ -3585,7 +3675,18 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + this.filterTextPacket(list).thenAcceptAsync((list1) -> { + this.updateSignText(packet, list1); +- }, this.server); ++ }, (Runnable run) -> { // Folia start - region threading ++ this.player.getBukkitEntity().taskScheduler.schedule( ++ (player) -> { ++ run.run(); ++ }, ++ null, 1L); ++ }).whenComplete((Object res, Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.error("Failed to handle sign update packet", thr); ++ } ++ }); ++ // Folia end - region threading + } + + private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 033755682c61c889723c3669b5cff4de147f637e..7fdb9304de7cf1979d57e3fac32415d7c674609d 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -111,7 +111,11 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + } + // Paper end - Do not allow logins while the server is shutting down + if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) { +- if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called ++ // Folia start - region threading - rewrite login process ++ String name = this.authenticatedProfile.getName(); ++ java.util.UUID uniqueId = this.authenticatedProfile.getId(); ++ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { ++ // Folia end - region threading - rewrite login process + this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile)); + } // Paper - prevent logins to be processed even though disconnect was called + } +@@ -257,7 +261,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + })); + } + +- boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference ++ boolean flag = false && playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here + + if (flag) { + this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; +@@ -377,7 +381,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + uniqueId = gameprofile.getId(); + // Paper end - Add more fields to AsyncPlayerPreLoginEvent + +- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading + final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); + if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { + event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure +diff --git a/src/main/java/net/minecraft/server/players/BanListEntry.java b/src/main/java/net/minecraft/server/players/BanListEntry.java +index 8b1da1fb5ca27432a39aff6dbc452b793268dab5..e83f3676d5a194fa8d3d1567edcb4b6f7847a4c1 100644 +--- a/src/main/java/net/minecraft/server/players/BanListEntry.java ++++ b/src/main/java/net/minecraft/server/players/BanListEntry.java +@@ -10,7 +10,7 @@ import net.minecraft.network.chat.Component; + + public abstract class BanListEntry extends StoredUserEntry { + +- public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); ++ public static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe + public static final String EXPIRES_NEVER = "forever"; + protected final Date created; + protected final String source; +@@ -32,7 +32,7 @@ public abstract class BanListEntry extends StoredUserEntry { + Date date; + + try { +- date = json.has("created") ? BanListEntry.DATE_FORMAT.parse(json.get("created").getAsString()) : new Date(); ++ date = json.has("created") ? BanListEntry.DATE_FORMAT.get().parse(json.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe + } catch (ParseException parseexception) { + date = new Date(); + } +@@ -43,7 +43,7 @@ public abstract class BanListEntry extends StoredUserEntry { + Date date1; + + try { +- date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.parse(json.get("expires").getAsString()) : null; ++ date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(json.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe + } catch (ParseException parseexception1) { + date1 = null; + } +@@ -78,9 +78,9 @@ public abstract class BanListEntry extends StoredUserEntry { + + @Override + protected void serialize(JsonObject json) { +- json.addProperty("created", BanListEntry.DATE_FORMAT.format(this.created)); ++ json.addProperty("created", BanListEntry.DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe + json.addProperty("source", this.source); +- json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.format(this.expires)); ++ json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe + json.addProperty("reason", this.reason); + } + +@@ -89,7 +89,7 @@ public abstract class BanListEntry extends StoredUserEntry { + Date expires = null; + + try { +- expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null; ++ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe + } catch (ParseException ex) { + // Guess we don't have a date + } +diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java +index 653856d0b8dcf2baf4cc77a276f17c8cc1fa717e..3f5639f26f249ca10e03826231d087ab715b7ce7 100644 +--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java ++++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java +@@ -516,7 +516,7 @@ public class OldUsersConverter { + Date date1; + + try { +- date1 = BanListEntry.DATE_FORMAT.parse(dateString); ++ date1 = BanListEntry.DATE_FORMAT.get().parse(dateString); // Folia - region threading - SDF is not thread-safe + } catch (ParseException parseexception) { + date1 = fallback; + } +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 9b71655a425356132afb786eff623f558e1e3498..b449de44b1911e2ff0701956bfba53fb5d2ed44e 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -134,10 +134,10 @@ public abstract class PlayerList { + public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login"); + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SEND_PLAYER_INFO_INTERVAL = 600; +- private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); ++ private static final ThreadLocal BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe + private final MinecraftServer server; + public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety +- private final Map playersByUUID = Maps.newHashMap(); ++ private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! + private final UserBanList bans; + private final IpBanList ipBans; + private final ServerOpList ops; +@@ -158,9 +158,63 @@ public abstract class PlayerList { + + // CraftBukkit start + private CraftServer cserver; +- private final Map playersByName = new java.util.HashMap<>(); ++ private final Map playersByName = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! + public @Nullable String collideRuleTeamName; // Paper - Configurable player collision + ++ // Folia start - region threading ++ private final Object connectionsStateLock = new Object(); ++ private final Map connectionByName = new java.util.HashMap<>(); ++ private final Map connectionById = new java.util.HashMap<>(); ++ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet usersCountedAgainstLimit = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); ++ ++ public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ Connection conflictingName, conflictingId; ++ synchronized (this.connectionsStateLock) { ++ conflictingName = this.connectionByName.get(userName); ++ conflictingId = this.connectionById.get(byId); ++ ++ if (conflictingName == null && conflictingId == null) { ++ this.connectionByName.put(userName, conn); ++ this.connectionById.put(byId, conn); ++ } ++ } ++ ++ Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); ++ ++ if (conflictingId != null || conflictingName != null) { ++ if (conflictingName != null && conflictingName.isPlayerConnected()) { ++ conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { ++ conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ } ++ ++ return conflictingName == null && conflictingId == null; ++ } ++ ++ public void removeConnection(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ synchronized (this.connectionsStateLock) { ++ this.connectionByName.remove(userName, conn); ++ this.connectionById.remove(byId, conn); ++ this.usersCountedAgainstLimit.remove(conn); ++ } ++ } ++ ++ private boolean countConnection(Connection conn, int limit) { ++ synchronized (this.connectionsStateLock) { ++ int count = this.usersCountedAgainstLimit.size(); ++ if (count >= limit) { ++ return false; ++ } ++ this.usersCountedAgainstLimit.add(conn); ++ return true; ++ } ++ } ++ // Folia end - region threading ++ + public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { + this.cserver = server.server = new CraftServer((DedicatedServer) server, this); + server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper +@@ -181,7 +235,7 @@ public abstract class PlayerList { + } + abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor + +- public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { ++ public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie clientData, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading - rewrite login process + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed + GameProfile gameprofile = player.getGameProfile(); +@@ -258,18 +312,42 @@ public abstract class PlayerList { + player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login + // Paper start - reset to main world spawn if first spawn or invalid world + } ++ // Folia start - region threading - rewrite login process ++ // must write to these before toComplete is invoked ++ data.setValue(optional.orElse(null)); ++ lastKnownName.setValue(s); ++ // Folia end - region threading - rewrite login process + if (optional.isEmpty() || invalidPlayerWorld[0]) { + // Paper end - reset to main world spawn if first spawn or invalid world +- player.moveTo(player.adjustSpawnLocation(worldserver1, worldserver1.getSharedSpawnPos()).getBottomCenter(), worldserver1.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored ++ ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading ++ } else { ++ worldserver1.loadChunksForMoveAsync( ++ player.getBoundingBox(), ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (c) -> { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position())); ++ } ++ ); + } ++ // Folia end - region threading - rewrite login process + // Paper end - Entity#getEntitySpawnReason ++ // Folia start - region threading - rewrite login process ++ return; ++ } ++ // optional -> player data ++ // s -> last known name ++ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData, Optional optional, String s, Location selectedSpawn) { ++ ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle(); ++ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); ++ player.lastSave = System.nanoTime(); // changed to nanoTime ++ // Folia end - region threading - rewrite login process + player.setServerLevel(worldserver1); + String s1 = connection.getLoggableAddress(this.server.logIPs()); + + // Spigot start - spawn location event + Player spawnPlayer = player.getBukkitEntity(); + org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); +- this.cserver.getPluginManager().callEvent(ev); ++ //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT? + + Location loc = ev.getSpawnLocation(); + worldserver1 = ((CraftWorld) loc.getWorld()).getHandle(); +@@ -288,6 +366,10 @@ public abstract class PlayerList { + + player.loadGameTypes((CompoundTag) optional.orElse(null)); // CraftBukkit - decompile error + ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, clientData); ++ // Folia start - rewrite login process ++ // only after setting the connection listener to game type, add the connection to this regions list ++ worldserver1.getCurrentWorldData().connections.add(connection); ++ // Folia end - rewrite login process + + connection.setupInboundProtocol(GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), playerconnection); + GameRules gamerules = worldserver1.getGameRules(); +@@ -307,7 +389,7 @@ public abstract class PlayerList { + this.sendPlayerPermissionLevel(player); + player.getStats().markAllDirty(); + player.getRecipeBook().sendInitialRecipeBook(player); +- this.updateEntireScoreboard(worldserver1.getScoreboard(), player); ++ if (false) this.updateEntireScoreboard(worldserver1.getScoreboard(), player); // Folia - region threading + this.server.invalidateStatus(); + MutableComponent ichatmutablecomponent; + +@@ -350,7 +432,7 @@ public abstract class PlayerList { + this.cserver.getPluginManager().callEvent(playerJoinEvent); + + if (!player.connection.isAcceptingMessages()) { +- return; ++ //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect + } + + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); +@@ -365,8 +447,7 @@ public abstract class PlayerList { + ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player + + final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer1 : this.players) { // Folia - region threading + + if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { + // Paper start - Add Listing API for Player +@@ -415,7 +496,7 @@ public abstract class PlayerList { + // Paper start - Configurable player collision; Add to collideRule team if needed + final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); +- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { ++ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end - Configurable player collision +@@ -518,7 +599,7 @@ public abstract class PlayerList { + + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit +- player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving ++ player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking + this.playerIo.save(player); + ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit + +@@ -558,7 +639,7 @@ public abstract class PlayerList { + // CraftBukkit end + + // Paper start - Configurable player collision; Remove from collideRule team if needed +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Folia - region threading + final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); + if (entityplayer.getTeam() == team && team != null) { +@@ -612,7 +693,7 @@ public abstract class PlayerList { + } + + worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); +- entityplayer.retireScheduler(); // Paper - Folia schedulers ++ // Folia - region threading - move to onDisconnect of common packet listener + entityplayer.getAdvancements().stopListening(); + this.players.remove(entityplayer); + this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -631,8 +712,7 @@ public abstract class PlayerList { + // CraftBukkit start + // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()))); + ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())); +- for (int i = 0; i < this.players.size(); i++) { +- ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer2 : this.players) { // Folia - region threading + + if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) { + entityplayer2.connection.send(packet); +@@ -657,19 +737,12 @@ public abstract class PlayerList { + + ServerPlayer entityplayer; + +- for (int i = 0; i < this.players.size(); ++i) { +- entityplayer = (ServerPlayer) this.players.get(i); +- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames +- list.add(entityplayer); +- } +- } ++ // Folia - region threading - rewrite login process - moved to pushPendingJoin + + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { +- entityplayer = (ServerPlayer) iterator.next(); +- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved +- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause ++ // Folia - moved to pushPendingJoin + } + + // Instead of kicking then returning, we need to store the kick reason +@@ -689,7 +762,7 @@ public abstract class PlayerList { + + ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason()); + if (gameprofilebanentry.getExpires() != null) { +- ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires()))); ++ ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.get().format(gameprofilebanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe + } + + // return chatmessage; +@@ -702,14 +775,14 @@ public abstract class PlayerList { + + ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason()); + if (ipbanentry.getExpires() != null) { +- ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires()))); ++ ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.get().format(ipbanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe + } + + // return chatmessage; + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { ++ if (!this.countConnection(loginlistener.connection, this.maxPlayers) && !this.canBypassPlayerLimit(gameprofile)) { // Folia - region threading - we control connection state here now async, not player list size + event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -769,6 +842,11 @@ public abstract class PlayerList { + } + + public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason, Location location) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + entityplayer.stopRiding(); // CraftBukkit + this.players.remove(entityplayer); + this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -945,10 +1023,10 @@ public abstract class PlayerList { + public void tick() { + if (++this.sendAllPlayerInfoIn > 600) { + // CraftBukkit start +- for (int i = 0; i < this.players.size(); ++i) { +- final ServerPlayer target = (ServerPlayer) this.players.get(i); ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading ++ for (final ServerPlayer target : players) { // Folia - region threading + +- target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { ++ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), java.util.Arrays.stream(players).filter(new Predicate() { // Folia - region threading + @Override + public boolean test(ServerPlayer input) { + return target.getBukkitEntity().canSee(input.getBukkitEntity()); +@@ -974,18 +1052,17 @@ public abstract class PlayerList { + + // CraftBukkit start - add a world/entity limited version + public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Folia - region threading + if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { + continue; + } +- ((ServerPlayer) this.players.get(i)).connection.send(packet); ++ entityplayer.connection.send(packet); // Folia - region threading + } + } + + public void broadcastAll(Packet packet, Level world) { +- for (int i = 0; i < world.players().size(); ++i) { +- ((ServerPlayer) world.players().get(i)).connection.send(packet); ++ for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading ++ ((ServerPlayer) player).connection.send(packet); // Folia - region threading + } + + } +@@ -1029,8 +1106,7 @@ public abstract class PlayerList { + if (scoreboardteam == null) { + this.broadcastSystemMessage(message, false); + } else { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Folia - region threading + + if (entityplayer.getTeam() != scoreboardteam) { + entityplayer.sendSystemMessage(message); +@@ -1041,10 +1117,12 @@ public abstract class PlayerList { + } + + public String[] getPlayerNamesArray() { +- String[] astring = new String[this.players.size()]; ++ List players = new java.util.ArrayList<>(this.players); // Folia start - region threading ++ String[] astring = new String[players.size()]; + +- for (int i = 0; i < this.players.size(); ++i) { +- astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName(); ++ for (int i = 0; i < players.size(); ++i) { ++ astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName(); ++ // Folia end - region threading + } + + return astring; +@@ -1063,7 +1141,9 @@ public abstract class PlayerList { + ServerPlayer entityplayer = this.getPlayer(profile.getId()); + + if (entityplayer != null) { ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + this.sendPlayerPermissionLevel(entityplayer); ++ }, null, 1L); // Folia - region threading + } + + } +@@ -1073,7 +1153,10 @@ public abstract class PlayerList { + ServerPlayer entityplayer = this.getPlayer(profile.getId()); + + if (entityplayer != null) { ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + this.sendPlayerPermissionLevel(entityplayer); ++ }, null, 1L); // Folia - region threading ++ + } + + } +@@ -1136,8 +1219,7 @@ public abstract class PlayerList { + } + + public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Folia - region threading + + // CraftBukkit start - Test if player receiving packet can see the source of the packet + if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) { +@@ -1166,10 +1248,15 @@ public abstract class PlayerList { + public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + int numSaved = 0; +- long now = MinecraftServer.currentTick; +- for (int i = 0; i < this.players.size(); ++i) { +- final ServerPlayer player = this.players.get(i); +- if (interval == -1 || now - player.lastSave >= interval) { ++ long now = System.nanoTime(); // Folia - region threading ++ long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading ++ for (ServerPlayer player : this.players) { // Folia - region threading ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { ++ continue; ++ } ++ // Folia end - region threading ++ if (interval == -1 || now - player.lastSave >= timeInterval) { // Folia - region threading + this.save(player); + if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } + } +@@ -1290,6 +1377,20 @@ public abstract class PlayerList { + } + + public void removeAll(boolean isRestarting) { ++ // Folia start - region threading ++ // just send disconnect packet, don't modify state ++ for (ServerPlayer player : this.players) { ++ final Component shutdownMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure ++ // CraftBukkit end ++ ++ player.connection.send(new net.minecraft.network.protocol.common.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> { ++ player.connection.connection.disconnect(shutdownMessage); ++ })); ++ } ++ if (true) { ++ return; ++ } ++ // Folia end - region threading + // Paper end + // CraftBukkit start - disconnect safely + for (ServerPlayer player : this.players) { +@@ -1299,7 +1400,7 @@ public abstract class PlayerList { + // CraftBukkit end + + // Paper start - Configurable player collision; Remove collideRule team if it exists +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Folia - region threading + final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); + if (team != null) scoreboard.removePlayerTeam(team); +diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java +index c038da20b76c0b7b1c18471b20be01e849d29f3a..87114cc9ce7489ff8e29e2d88ebb0d47eb68232e 100644 +--- a/src/main/java/net/minecraft/server/players/StoredUserList.java ++++ b/src/main/java/net/minecraft/server/players/StoredUserList.java +@@ -103,6 +103,7 @@ public abstract class StoredUserList> { + } + + public void save() throws IOException { ++ synchronized (this) { // Folia - region threading + this.removeExpired(); // Paper - remove expired values before saving + JsonArray jsonarray = new JsonArray(); + Stream stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error +@@ -133,10 +134,12 @@ public abstract class StoredUserList> { + if (bufferedwriter != null) { + bufferedwriter.close(); + } ++ } // Folia - region threading + + } + + public void load() throws IOException { ++ synchronized (this) { // Folia - region threading + if (this.file.exists()) { + BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8); + +@@ -193,5 +196,6 @@ public abstract class StoredUserList> { + } + + } ++ } // Folia - region threading + } + } +diff --git a/src/main/java/net/minecraft/util/SpawnUtil.java b/src/main/java/net/minecraft/util/SpawnUtil.java +index a8903f087c75c851e8111b96e5aa9f271d618392..27fb5cd80b33fdca0ff8785072c469ff9a42aa6a 100644 +--- a/src/main/java/net/minecraft/util/SpawnUtil.java ++++ b/src/main/java/net/minecraft/util/SpawnUtil.java +@@ -61,7 +61,7 @@ public class SpawnUtil { + return Optional.of(t0); + } + +- t0.discard(null); // CraftBukkit - add Bukkit remove cause ++ //t0.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading + } + } + } +diff --git a/src/main/java/net/minecraft/world/RandomSequences.java b/src/main/java/net/minecraft/world/RandomSequences.java +index 75631d9368bfc4baec1631db4f7dbfcc2c6d8ac5..320e113db209881d5dd619c6056a19a18b988e4c 100644 +--- a/src/main/java/net/minecraft/world/RandomSequences.java ++++ b/src/main/java/net/minecraft/world/RandomSequences.java +@@ -21,7 +21,7 @@ public class RandomSequences extends SavedData { + private int salt; + private boolean includeWorldSeed = true; + private boolean includeSequenceId = true; +- private final Map sequences = new Object2ObjectOpenHashMap<>(); ++ private final Map sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading + + public static SavedData.Factory factory(long seed) { + return new SavedData.Factory<>(() -> new RandomSequences(seed), (nbt, registries) -> load(seed, nbt), DataFixTypes.SAVED_DATA_RANDOM_SEQUENCES); +@@ -114,61 +114,61 @@ public class RandomSequences extends SavedData { + @Override + public RandomSource fork() { + RandomSequences.this.setDirty(); +- return this.random.fork(); ++ synchronized (this.random) { return this.random.fork(); } // Folia - region threading + } + + @Override + public PositionalRandomFactory forkPositional() { + RandomSequences.this.setDirty(); +- return this.random.forkPositional(); ++ synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading + } + + @Override + public void setSeed(long seed) { + RandomSequences.this.setDirty(); +- this.random.setSeed(seed); ++ synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading + } + + @Override + public int nextInt() { + RandomSequences.this.setDirty(); +- return this.random.nextInt(); ++ synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading + } + + @Override + public int nextInt(int bound) { + RandomSequences.this.setDirty(); +- return this.random.nextInt(bound); ++ synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading + } + + @Override + public long nextLong() { + RandomSequences.this.setDirty(); +- return this.random.nextLong(); ++ synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading + } + + @Override + public boolean nextBoolean() { + RandomSequences.this.setDirty(); +- return this.random.nextBoolean(); ++ synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading + } + + @Override + public float nextFloat() { + RandomSequences.this.setDirty(); +- return this.random.nextFloat(); ++ synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading + } + + @Override + public double nextDouble() { + RandomSequences.this.setDirty(); +- return this.random.nextDouble(); ++ synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading + } + + @Override + public double nextGaussian() { + RandomSequences.this.setDirty(); +- return this.random.nextGaussian(); ++ synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +index 99a7e9eb75231c15bd8bb24fbb4e296bc9fdedff..b4a081392b68ccb869392f93ee1f259f0d4f6adc 100644 +--- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java ++++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +@@ -53,7 +53,7 @@ public class CombatTracker { + } + + private Component getMessageForAssistedFall(Entity attacker, Component attackerDisplayName, String itemDeathTranslationKey, String deathTranslationKey) { +- ItemStack itemStack = attacker instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; ++ ItemStack itemStack = attacker instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity) ? livingEntity.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading + return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) + ? Component.translatable(itemDeathTranslationKey, this.mob.getDisplayName(), attackerDisplayName, itemStack.getDisplayName()) + : Component.translatable(deathTranslationKey, this.mob.getDisplayName(), attackerDisplayName); +@@ -80,7 +80,7 @@ public class CombatTracker { + + @Nullable + private static Component getDisplayName(@Nullable Entity entity) { +- return entity == null ? null : entity.getDisplayName(); ++ return entity == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading + } + + public Component getDeathMessage() { +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +index bb1a60180e58c1333e7bb33e8acf1b0225eda8a8..bc7568c26e6f2b64365712b31d5fce708a0a272d 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +@@ -178,13 +178,13 @@ public class DamageSource { + LivingEntity entityliving1 = killed.getKillCredit(); + String s1 = s + ".player"; + +- return entityliving1 != null ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName()); ++ return entityliving1 != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityliving1) ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName()); // Folia - region threading + } else { + Component ichatbasecomponent = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName(); + Entity entity = this.causingEntity; + ItemStack itemstack; + +- if (entity instanceof LivingEntity) { ++ if (entity instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading + LivingEntity entityliving2 = (LivingEntity) entity; + + itemstack = entityliving2.getMainHandItem(); +diff --git a/src/main/java/net/minecraft/world/damagesource/FallLocation.java b/src/main/java/net/minecraft/world/damagesource/FallLocation.java +index e9df8f8541b8a1b85c7d2925ff3cba813007a1ef..d3f2775a68121ca80ef55ea4c280a0c9fcae2db3 100644 +--- a/src/main/java/net/minecraft/world/damagesource/FallLocation.java ++++ b/src/main/java/net/minecraft/world/damagesource/FallLocation.java +@@ -35,7 +35,7 @@ public record FallLocation(String id) { + @Nullable + public static FallLocation getCurrentFallLocation(LivingEntity entity) { + Optional optional = entity.getLastClimbablePos(); +- if (optional.isPresent()) { ++ if (optional.isPresent() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), optional.get())) { // Folia - region threading + BlockState blockState = entity.level().getBlockState(optional.get()); + return blockToFallLocation(blockState); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 7ac7d0729705cb02f22277be3c467aed4f69ec0e..35ff983cd84cb610b70e193220a97a3a5406252f 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -186,7 +186,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + // Paper start - Share random for entities to make them more random +- public static RandomSource SHARED_RANDOM = new RandomRandomSource(); ++ public static RandomSource SHARED_RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading + // Paper start - replace random + private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom { + public RandomRandomSource() { +@@ -216,7 +216,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason + + public boolean collisionLoadChunks = false; // Paper +- private CraftEntity bukkitEntity; ++ private volatile CraftEntity bukkitEntity; // Folia - region threading + + public CraftEntity getBukkitEntity() { + if (this.bukkitEntity == null) { +@@ -339,7 +339,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + private boolean hasGlowingTag; + private final Set tags; + private final double[] pistonDeltas; +- private long pistonDeltasGameTime; ++ private long pistonDeltasGameTime = Long.MIN_VALUE; // Folia - region threading + private EntityDimensions dimensions; + private float eyeHeight; + public boolean isInPowderSnow; +@@ -568,6 +568,19 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } + // Paper end - optimise entity tracker ++ // Folia start - region ticking ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ if (this.activatedTick != Integer.MIN_VALUE) { ++ this.activatedTick += fromTickOffset; ++ } ++ if (this.activatedImmunityTick != Integer.MIN_VALUE) { ++ this.activatedImmunityTick += fromTickOffset; ++ } ++ if (this.pistonDeltasGameTime != Long.MIN_VALUE) { ++ this.pistonDeltasGameTime += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region ticking + + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); +@@ -719,8 +732,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // due to interactions on the client. + public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { + if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { +- ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level(); +- net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId()); ++ net.minecraft.server.level.ChunkMap.TrackedEntity tracker = this.moonrise$getTrackedEntity(); // Folia - region threading + if (tracker == null) { + return; + } +@@ -887,7 +899,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public void postTick() { + // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle + if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities +- this.handlePortal(); ++ //this.handlePortal(); // Folia - region threading + } + } + // CraftBukkit end +@@ -906,7 +918,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + --this.boardingCooldown; + } + +- if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick ++ //if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick() + if (this.canSpawnSprintParticle()) { + this.spawnSprintParticle(); + } +@@ -1186,8 +1198,8 @@ 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 ++ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading + movement = this.limitPistonMovement(movement); + if (movement.equals(Vec3.ZERO)) { + return; +@@ -1501,7 +1513,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + if (movement.lengthSqr() <= 1.0E-7D) { + return movement; + } else { +- long i = this.level().getGameTime(); ++ long i = this.level().getRedstoneGameTime(); // Folia - region threading + + if (i != this.pistonDeltasGameTime) { + Arrays.fill(this.pistonDeltas, 0.0D); +@@ -3324,7 +3336,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.passengers = ImmutableList.copyOf(list); + } + +- this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); ++ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added + } + } + +@@ -3372,7 +3384,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + entity.boardingCooldown = 60; +- this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); ++ if (!entity.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); // Folia - region threading - do not fire game events for entities not added + } + return true; // CraftBukkit + } +@@ -3459,7 +3471,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } + +- protected void handlePortal() { ++ public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean + Level world = this.level(); + + if (world instanceof ServerLevel worldserver) { +@@ -3470,23 +3482,21 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + gameprofilerfiller.push("portal"); + this.setPortalCooldown(); +- TeleportTransition teleporttransition = this.portalProcess.getPortalDestination(worldserver, this); +- +- if (teleporttransition != null) { +- ServerLevel worldserver1 = teleporttransition.newLevel(); +- +- if (this instanceof ServerPlayer || (worldserver1 != null && (worldserver1.dimension() == worldserver.dimension() || this.canTeleport(worldserver, worldserver1)))) { // CraftBukkit - always call event for players +- this.teleport(teleporttransition); +- } ++ // Folia start - region threading ++ try { ++ return this.portalProcess.portalAsync(worldserver, this); ++ } finally { ++ gameprofilerfiller.pop(); + } +- +- gameprofilerfiller.pop(); ++ // Folia end - region threading + } else if (this.portalProcess.hasExpired()) { + this.portalProcess = null; + } + + } + } ++ ++ return false; // Folia - region threading + } + + public int getDimensionChangingDelay() { +@@ -3627,6 +3637,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + @Nullable + public PlayerTeam getTeam() { ++ // Folia start - region threading ++ if (true) { ++ return null; ++ } ++ // Folia end - region threading + if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default + return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName()); + } +@@ -3920,8 +3935,782 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.portalProcess = original.portalProcess; + } + ++ // Folia start - region threading ++ public static class EntityTreeNode { ++ @Nullable ++ public EntityTreeNode parent; ++ public Entity root; ++ @Nullable ++ public EntityTreeNode[] passengers; ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root) { ++ this.parent = parent; ++ this.root = root; ++ } ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { ++ this.parent = parent; ++ this.root = root; ++ this.passengers = passengers; ++ } ++ ++ public List getFullTree() { ++ List ret = new java.util.ArrayList<>(); ++ ret.add(this); ++ ++ // this is just a BFS except we don't remove from head, we just advance down the list ++ for (int i = 0; i < ret.size(); ++i) { ++ EntityTreeNode node = ret.get(i); ++ ++ EntityTreeNode[] passengers = node.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ for (EntityTreeNode passenger : passengers) { ++ ret.add(passenger); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void restore() { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ List newPassengers = new java.util.ArrayList<>(); ++ for (EntityTreeNode passenger : passengers) { ++ newPassengers.add(passenger.root); ++ passenger.root.vehicle = curr.root; ++ } ++ ++ curr.root.passengers = ImmutableList.copyOf(newPassengers); ++ } ++ } ++ ++ public void addTracker() { ++ for (final EntityTreeNode node : this.getFullTree()) { ++ if (node.root.moonrise$getTrackedEntity() != null) { ++ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { ++ node.root.moonrise$getTrackedEntity().updatePlayer(player); ++ } ++ } ++ } ++ } ++ ++ public void clearTracker() { ++ for (final EntityTreeNode node : this.getFullTree()) { ++ if (node.root.moonrise$getTrackedEntity() != null) { ++ node.root.moonrise$getTrackedEntity().moonrise$removeNonTickThreadPlayers(); ++ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { ++ node.root.moonrise$getTrackedEntity().removePlayer(player); ++ } ++ } ++ } ++ } ++ ++ public void adjustRiders(boolean teleport) { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ for (EntityTreeNode passenger : passengers) { ++ curr.root.positionRider(passenger.root, teleport ? Entity::moveTo : Entity::setPos); ++ } ++ } ++ } ++ } ++ ++ public void repositionAllPassengers(boolean teleport) { ++ this.makePassengerTree().adjustRiders(teleport); ++ } ++ ++ protected EntityTreeNode makePassengerTree() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ protected EntityTreeNode detachPassengers() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ vehicle.passengers = ImmutableList.of(); ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ passenger.vehicle = null; ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ /** ++ * This flag will perform an async load on the chunks determined by ++ * the entity's bounding box before teleporting the entity. ++ */ ++ public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; ++ /** ++ * This flag requires the entity being teleported to be a root vehicle. ++ * Thus, if you want to teleport a non-root vehicle, you must dismount ++ * the target entity before calling teleport, otherwise the ++ * teleport will be refused. ++ */ ++ public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; ++ /** ++ * The flag will dismount any passengers and dismout from the current vehicle ++ * to teleport if and only if dismounting would result in the teleport being allowed. ++ */ ++ public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2; ++ ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ destination.addDuringTeleport(this); ++ } ++ ++ protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, ++ EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { ++ Vec3 pos = this.position(); ++ ChunkPos posChunk = new ChunkPos( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) ++ ); ++ ++ // ensure the region is always ticking in case of a shutdown ++ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region ++ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); ++ originWorld.chunkSource.addTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, posChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos); ++ destination.pushPendingTeleport(pendingTeleport); ++ ++ Runnable scheduleEntityJoin = () -> { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ destination, ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos), ++ () -> { ++ if (!destination.removePendingTeleport(pendingTeleport)) { ++ // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure ++ // we do not produce any errors here ++ return; ++ } ++ originWorld.chunkSource.removeTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, posChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ List fullTree = passengerTree.getFullTree(); ++ for (EntityTreeNode node : fullTree) { ++ node.root.placeSingleSync(originWorld, destination, node, teleportFlags); ++ } ++ ++ // restore passenger tree ++ passengerTree.restore(); ++ passengerTree.adjustRiders(true); ++ ++ // invoke post dimension change now ++ for (EntityTreeNode node : fullTree) { ++ node.root.postChangeDimension(); ++ } ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(Entity.this); ++ } ++ } ++ ); ++ }; ++ ++ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { ++ destination.loadChunksForMoveAsync( ++ this.getBoundingBox(), ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (chunkList) -> { ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { ++ destination.chunkSource.addTicketAtLevel( ++ TicketType.POST_TELEPORT, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ Integer.valueOf(Entity.this.getId()) ++ ); ++ } ++ scheduleEntityJoin.run(); ++ } ++ ); ++ } else { ++ scheduleEntityJoin.run(); ++ } ++ } ++ ++ protected boolean canTeleportAsync() { ++ return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); ++ } ++ ++ // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with ++ // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position ++ // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks. ++ protected void resetStoredPositions() { ++ this.mainSupportingBlockPos = Optional.empty(); ++ } ++ ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ this.moveTo(pos.x, pos.y, pos.z); ++ this.setOldPosAndRot(); ++ this.resetStoredPositions(); ++ } ++ ++ protected final void transform(TeleportTransition telpeort) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( ++ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() ++ ); ++ this.transform( ++ move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() ++ ); ++ } ++ ++ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ if (pos != null) { ++ this.setPosRaw(pos.x, pos.y, pos.z); ++ } ++ this.setOldPosAndRot(); ++ } ++ ++ protected final Entity transformForAsyncTeleport(TeleportTransition telpeort) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( ++ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() ++ ); ++ return this.transformForAsyncTeleport( ++ telpeort.newLevel(), telpeort.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() ++ ); ++ } ++ ++ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying ++ ++ Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL); ++ copy.restoreFrom(this); ++ copy.transform(pos, yaw, pitch, velocity); ++ // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here ++ // for example, clearing of inventory after switching dimensions ++ this.postRemoveAfterChangingDimensions(); ++ ++ return copy; ++ } ++ ++ public final boolean teleportAsync(TeleportTransition teleportTarget, long teleportFlags, ++ java.util.function.Consumer teleportComplete) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); ++ ++ return this.teleportAsync( ++ teleportTarget.newLevel(), move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement(), ++ teleportTarget.cause(), teleportFlags, teleportComplete ++ ); ++ } ++ ++ public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, ++ java.util.function.Consumer teleportComplete) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); ++ ++ if (!ServerLevel.isInSpawnableBounds(new BlockPos(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockY(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockZ(pos)))) { ++ return false; ++ } ++ ++ if (!this.canTeleportAsync()) { ++ return false; ++ } ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) { ++ for (Entity entity : this.getIndirectPassengers()) { ++ if (!entity.canTeleportAsync()) { ++ return false; ++ } ++ entity.getBukkitEntity(); // force bukkit entity to be created before TPing ++ } ++ } else { ++ this.unRide(); ++ } ++ ++ if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { ++ if (this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || this.isPassenger()) { ++ return false; ++ } ++ } ++ ++ // TODO any events that can modify go HERE ++ ++ // check for same region ++ if (destination == this.level()) { ++ Vec3 currPos = this.position(); ++ if ( ++ destination.regioniser.getRegionAtUnsynchronised( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(currPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(currPos) ++ ) == destination.regioniser.getRegionAtUnsynchronised( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) ++ ) ++ ) { ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ // Note: The client does not accept position updates for controlled entities. So, we must ++ // perform a lot of tracker updates here to make it all work out. ++ ++ // first, clear the tracker ++ passengerTree.clearTracker(); ++ for (EntityTreeNode entity : passengerTree.getFullTree()) { ++ entity.root.teleportSyncSameRegion(pos, yaw, pitch, velocity); ++ } ++ ++ passengerTree.restore(); ++ // re-add to the tracker once the tree is restored ++ passengerTree.addTracker(); ++ ++ // adjust entities to final position ++ passengerTree.adjustRiders(true); ++ ++ // the tracker clear/add logic is only used in the same region, as the other logic ++ // performs add/remove from world logic which will also perform add/remove tracker logic ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(this); ++ } ++ return true; ++ } ++ } ++ ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, velocity); ++ } ++ ++ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); ++ ++ return true; ++ } ++ ++ public void preChangeDimension() { ++ if (this instanceof Leashable leashable) { ++ leashable.dropLeash(); ++ } ++ } ++ ++ public void postChangeDimension() { ++ this.resetStoredPositions(); ++ } ++ ++ protected static enum PortalType { ++ NETHER, END; ++ } ++ ++ public boolean endPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? Level.OVERWORLD : Level.END); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, portalPos, true, PortalType.END, null); ++ } ++ ++ public boolean netherPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, portalPos, true, PortalType.NETHER, null); ++ } ++ ++ private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong(); ++ private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong(); ++ ++ // To simplify portal logic, in region threading both players ++ // and non-player entities will create portals. By guaranteeing ++ // that the teleportation can take place, we can simply ++ // remove the entity, find/create the portal, and place async. ++ // If we have to worry about whether the entity may not teleport, ++ // we need to first search, then report back, ... ++ protected void findOrCreatePortalAsync(ServerLevel origin, BlockPos originPortal, ServerLevel destination, PortalType type, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable) { ++ switch (type) { ++ // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld ++ // we just select the spawn position ++ case END: { ++ if (destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { ++ BlockPos targetPos = ServerLevel.END_SPAWN_POINT; ++ // need to load chunks so we can create the platform ++ destination.moonrise$loadChunksAsync( ++ targetPos, 16, // load 16 blocks to be safe from block physics ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); ++ ++ // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity ++ // on the obsidian, we need to spawn at targetPos.y - 1 ++ portalInfoCompletable.complete( ++ new net.minecraft.world.level.portal.TeleportTransition( ++ destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL ++ ) ++ ); ++ } ++ ); ++ } else { ++ BlockPos spawnPos = destination.getSharedSpawnPos(); ++ // need to load chunk for heightmap ++ destination.moonrise$loadChunksAsync( ++ spawnPos, 0, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); ++ ++ // done ++ portalInfoCompletable.complete( ++ new net.minecraft.world.level.portal.TeleportTransition( ++ destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL ++ ) ++ ); ++ } ++ ); ++ } ++ ++ break; ++ } ++ // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) ++ // then we can search for an existing portal using the POI routines ++ // if we don't find a portal, then we bring the chunks in the create radius to full and ++ // create it ++ case NETHER: { ++ // hoisted from the create fallback, so that we can avoid the sync load later if we need it ++ BlockState originalPortalBlock = origin.getBlockStateIfLoaded(originPortal); ++ Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : ++ originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); ++ BlockUtil.FoundRectangle originalPortalRectangle = ++ originalPortalBlock == null || !originalPortalBlock.hasProperty(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_AXIS) ++ ? null ++ : BlockUtil.getLargestRectangleAround( ++ originPortal, originalPortalDirection, 21, Direction.Axis.Y, 21, ++ (blockpos) -> { ++ return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock; ++ } ++ ); ++ ++ boolean destinationIsNether = destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; ++ ++ int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? ++ (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : ++ destination.paperConfig().environment.portalSearchRadius; ++ int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; ++ ++ WorldBorder destinationBorder = destination.getWorldBorder(); ++ double dimensionScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); ++ BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalFound ++ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ // post portal find/create logic ++ portalFound.addWaiter( ++ (BlockUtil.FoundRectangle portal, Throwable thr) -> { ++ // no portal could be created ++ if (portal == null) { ++ portalInfoCompletable.complete( ++ new TeleportTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO, ++ 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET)) ++ ); ++ return; ++ } ++ ++ Vec3 relativePos = originalPortalRectangle == null ? ++ new Vec3(0.5, 0.0, 0.0) : ++ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); ++ ++ portalInfoCompletable.complete( ++ net.minecraft.world.level.block.NetherPortalBlock.createDimensionTransition( ++ destination, portal, originalPortalDirection, relativePos, ++ Entity.this, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET) ++ ) ++ ); ++ } ++ ); ++ ++ // kick off search for existing portal or creation ++ destination.moonrise$loadChunksAsync( ++ // add 32 so that the final search for a portal frame doesn't load any chunks ++ targetPos, portalSearchRadius + 32, ++ net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ BlockUtil.FoundRectangle portal = ++ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); ++ if (portal != null) { ++ portalFound.complete(portal); ++ return; ++ } ++ ++ // add tickets so that we can re-search for a portal once the chunks are loaded ++ Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement()); ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { ++ destination.chunkSource.addTicketAtLevel( ++ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ ticketId ++ ); ++ } ++ ++ // no portal found - create one ++ destination.moonrise$loadChunksAsync( ++ targetPos, portalCreateRadius + 32, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks2) -> { ++ // don't need the tickets anymore ++ // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically ++ // if the ticket level were to decrease ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { ++ destination.chunkSource.removeTicketAtLevel( ++ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ ticketId ++ ); ++ } ++ ++ // when two entities portal at the same time, it is possible that both entities reach this ++ // part of the code - and create a double portal ++ // to fix this, we just issue another search to try and see if another entity created ++ // a portal nearby ++ BlockUtil.FoundRectangle existingTryAgain = ++ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); ++ if (existingTryAgain != null) { ++ portalFound.complete(existingTryAgain); ++ return; ++ } ++ ++ // we do not have the correct entity reference here ++ BlockUtil.FoundRectangle createdPortal = ++ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); ++ // if it wasn't created, passing null is expected here ++ portalFound.complete(createdPortal); ++ } ++ ); ++ } ++ ); ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown portal type " + type); ++ } ++ } ++ } ++ ++ public boolean canPortalAsync(ServerLevel to, boolean considerPassengers) { ++ return this.canPortalAsync(to, considerPassengers, false); ++ } ++ ++ protected boolean canPortalAsync(ServerLevel to, boolean considerPassengers, boolean skipPassengerCheck) { ++ if (considerPassengers) { ++ if (!skipPassengerCheck && this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { ++ return false; ++ } ++ } ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if (!this.canTeleportAsync()) { ++ return false; ++ } ++ if (considerPassengers) { ++ for (Entity entity : this.passengers) { ++ if (!entity.canPortalAsync(to, true, true)) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ ++ } ++ ++ protected boolean portalToAsync(ServerLevel destination, BlockPos portalPos, boolean takePassengers, ++ PortalType type, java.util.function.Consumer teleportComplete) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ if (!this.canPortalAsync(destination, takePassengers)) { ++ return false; ++ } ++ ++ Vec3 initialPosition = this.position(); ++ ChunkPos initialPositionChunk = new ChunkPos( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(initialPosition) ++ ); ++ ++ // first, remove entity/passengers from world ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ node.root.prePortalLogic(originWorld, destination, type); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ // we will update pos/rot/speed later ++ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); ++ // set portal cooldown ++ node.root.setPortalCooldown(); ++ } ++ ++ // ensure the region is always ticking in case of a shutdown ++ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region ++ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); ++ originWorld.chunkSource.addTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ ++ ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition); ++ originWorld.pushPendingTeleport(beforeFindDestination); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable ++ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ portalInfoCompletable.addWaiter((TeleportTransition info, Throwable throwable) -> { ++ if (!originWorld.removePendingTeleport(beforeFindDestination)) { ++ // the shutdown thread has placed us back into the origin world at the original position ++ // we just have to abandon this teleport to prevent duplication ++ return; ++ } ++ originWorld.chunkSource.removeTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ // adjust passenger tree to final pos/rot/speed ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.transform(info); ++ } ++ ++ // place ++ passengerTree.root.placeInAsync( ++ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), ++ passengerTree, ++ (Entity teleported) -> { ++ if (info.postTeleportTransition() != null) { ++ info.postTeleportTransition().onTransition(teleported); ++ } ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(teleported); ++ } ++ } ++ ); ++ }); ++ ++ ++ passengerTree.root.findOrCreatePortalAsync(originWorld, portalPos, destination, type, portalInfoCompletable); ++ ++ return true; ++ } ++ // Folia end - region threading ++ + @Nullable + public Entity teleport(TeleportTransition teleportTarget) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + Level world = this.level(); + + // Paper start - Fix item duplication and teleport issues +@@ -4126,6 +4915,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + } + ++ // Folia start - region threading - move inventory clearing until after the dimension change ++ protected void postRemoveAfterChangingDimensions() { ++ ++ } ++ // Folia end - region threading - move inventory clearing until after the dimension change ++ + protected void removeAfterChangingDimensions() { + this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause + if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed +@@ -5059,7 +5854,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + } + // Paper end - Fix MC-4 +- if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; // Folia - region threading ++ if (posChanged) { // Folia - region threading + synchronized (this.posLock) { // Paper + this.position = new Vec3(x, y, z); + } // Paper +@@ -5080,7 +5876,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB + // hanging has its own special logic +- if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { ++ if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end - Block invalid positions and bounding box +@@ -5163,6 +5959,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.removalReason != null; + } + ++ // Folia start - region threading ++ public final boolean hasNullCallback() { ++ return this.levelCallback == EntityInLevelCallback.NULL; ++ } ++ // Folia end - region threading ++ + @Nullable + public Entity.RemovalReason getRemovalReason() { + return this.removalReason; +@@ -5185,6 +5987,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end + final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers ++ // Folia start - region threading ++ this.preRemove(entity_removalreason); ++ // Folia end - region threading + if (this.removalReason == null) { + this.removalReason = entity_removalreason; + } +@@ -5208,6 +6013,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.removalReason = null; + } + ++ // Folia start - region threading ++ protected void preRemove(Entity.RemovalReason reason) {} ++ // Folia end - region threading ++ + // Paper start - Folia schedulers + /** + * Invoked only when the entity is truly removed from the server, never to be added to any world. +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 96b4fbe4a4655777ff10b32e3257e2fac2aba12a..e6871fb4b58910043e88ea45564363aa854eb0ca 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -274,7 +274,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + private Optional lastClimbablePos; + @Nullable + private DamageSource lastDamageSource; +- private long lastDamageStamp; ++ private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading + protected int autoSpinAttackTicks; + protected float autoSpinAttackDmg; + @Nullable +@@ -308,6 +308,21 @@ public abstract class LivingEntity extends Entity implements Attackable { + ++this.noActionTime; // Above all the floats + } + // Spigot end ++ // Folia start - region threading ++ @Override ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.lastDamageStamp != Long.MIN_VALUE) { ++ this.lastDamageStamp += fromRedstoneTimeOffset; ++ } ++ } ++ ++ @Override ++ protected void resetStoredPositions() { ++ super.resetStoredPositions(); ++ this.lastClimbablePos = Optional.empty(); ++ } ++ // Folia end - region threading + + protected LivingEntity(EntityType type, Level world) { + super(type, world); +@@ -535,7 +550,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) { + this.tickDeath(); +- } ++ } else { this.broadcastedDeath = false; } // Folia - region threading + + if (this.lastHurtByPlayerTime > 0) { + --this.lastHurtByPlayerTime; +@@ -625,11 +640,14 @@ public abstract class LivingEntity extends Entity implements Attackable { + return true; + } + ++ public boolean broadcastedDeath = false; // Folia - region threading + protected void tickDeath() { + ++this.deathTime; +- if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) { ++ if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved() && !this.broadcastedDeath) { // Folia - region threading + this.level().broadcastEntityEvent(this, (byte) 60); +- this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause ++ this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted ++ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running ++ if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead + } + + } +@@ -888,9 +906,9 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.hurtTime = nbt.getShort("HurtTime"); +- this.deathTime = nbt.getShort("DeathTime"); ++ this.deathTime = nbt.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading + this.lastHurtByMobTimestamp = nbt.getInt("HurtByTimestamp"); +- if (nbt.contains("Team", 8)) { ++ if (false && nbt.contains("Team", 8)) { // Folia start - region threading + String s = nbt.getString("Team"); + Scoreboard scoreboard = this.level().getScoreboard(); + PlayerTeam scoreboardteam = scoreboard.getPlayerTeam(s); +@@ -1167,6 +1185,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { + // Paper end - Don't fire sync event during generation + // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API ++ if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading + if (this.isTickingEffects) { + this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause)); + return true; +@@ -1596,7 +1615,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (flag2) { + this.lastDamageSource = source; +- this.lastDamageStamp = this.level().getGameTime(); ++ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading + Iterator iterator = this.getActiveEffects().iterator(); + + while (iterator.hasNext()) { +@@ -1748,7 +1767,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Nullable + public DamageSource getLastDamageSource() { +- if (this.level().getGameTime() - this.lastDamageStamp > 40L) { ++ if (this.level().getRedstoneGameTime() - this.lastDamageStamp > 40L || this.lastDamageStamp == Long.MIN_VALUE) { // Folia - region threading + this.lastDamageSource = null; + } + +@@ -2560,7 +2579,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + + @Nullable + public LivingEntity getKillCredit() { +- return (LivingEntity) (this.lastHurtByPlayer != null ? this.lastHurtByPlayer : (this.lastHurtByMob != null ? this.lastHurtByMob : null)); ++ return (LivingEntity) (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer) ? this.lastHurtByPlayer : (this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null)); // Folia - region threading + } + + public final float getMaxHealth() { +@@ -2637,7 +2656,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.lastDamageSource = damageSource; +- this.lastDamageStamp = this.level().getGameTime(); ++ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading + } + + @Override +@@ -3703,7 +3722,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.pushEntities(); + gameprofilerfiller.pop(); + // Paper start - Add EntityMoveEvent +- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { ++ if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { // Folia - region threading + if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { + Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); +@@ -4413,7 +4432,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + BlockPos blockposition = BlockPos.containing(d0, d1, d2); + Level world = this.level(); + +- if (world.hasChunkAt(blockposition)) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition) && world.hasChunkAt(blockposition)) { // Folia - region threading + boolean flag2 = false; + + while (!flag2 && blockposition.getY() > world.getMinY()) { +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 5a0b51342f4a646101f4588697bcae7d1ca8a010..e48728723e9f765099fc1cea8e6a2baa48d7fc75 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -292,9 +292,21 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + @Nullable + @Override + public LivingEntity getTarget() { ++ // Folia start - region threading ++ if (this.target != null && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) { ++ this.target = null; ++ return null; ++ } ++ // Folia end - region threading + return this.target; + } + ++ // Folia start - region threading ++ public LivingEntity getTargetRaw() { ++ return this.target; ++ } ++ // Folia end - region threading ++ + @Nullable + protected final LivingEntity getTargetFromBrain() { + return (LivingEntity) this.getBrain().getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null); // CraftBukkit - decompile error +@@ -306,7 +318,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + } + + public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) { +- if (this.getTarget() == entityliving) return false; ++ if (this.getTargetRaw() == entityliving) return false; // Folia - region threading + if (fireEvent) { + if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && entityliving == null) { + reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED; +@@ -1779,16 +1791,22 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + this.goalSelector.removeAllGoals(predicate); + } + ++ // Folia start - region threading - move inventory clearing until after the dimension change + @Override +- protected void removeAfterChangingDimensions() { +- super.removeAfterChangingDimensions(); ++ protected void postRemoveAfterChangingDimensions() { + this.getAllSlots().forEach((itemstack) -> { + if (!itemstack.isEmpty()) { + itemstack.setCount(0); + } +- + }); + } ++ // Folia end - region threading - move inventory clearing until after the dimension change ++ ++ @Override ++ protected void removeAfterChangingDimensions() { ++ super.removeAfterChangingDimensions(); ++ // Folia - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions ++ } + + @Nullable + @Override +diff --git a/src/main/java/net/minecraft/world/entity/PortalProcessor.java b/src/main/java/net/minecraft/world/entity/PortalProcessor.java +index b4a8249964786d484aa0767d0e73d71d2156f0e8..0ee15cb6cc2698c511b2ba274f976d32bb5fbf4c 100644 +--- a/src/main/java/net/minecraft/world/entity/PortalProcessor.java ++++ b/src/main/java/net/minecraft/world/entity/PortalProcessor.java +@@ -33,6 +33,12 @@ public class PortalProcessor { + return this.portal.getPortalDestination(world, entity, this.entryPosition); + } + ++ // Folia start - region threading ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget) { ++ return this.portal.portalAsync(sourceWorld, portalTarget, this.entryPosition); ++ } ++ // Folia end - region threading ++ + public Portal.Transition getPortalLocalTransition() { + return this.portal.getLocalTransition(); + } +diff --git a/src/main/java/net/minecraft/world/entity/TamableAnimal.java b/src/main/java/net/minecraft/world/entity/TamableAnimal.java +index 68224d1cdabdaf04e4d26dd07e222a312e8fc898..d66795269c955db6b871143a63b5e7fde3d68289 100644 +--- a/src/main/java/net/minecraft/world/entity/TamableAnimal.java ++++ b/src/main/java/net/minecraft/world/entity/TamableAnimal.java +@@ -288,6 +288,11 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { + LivingEntity entityliving = this.getOwner(); + + if (entityliving != null) { ++ // Folia start - region threading ++ if (entityliving.isRemoved() || entityliving.level() != this.level()) { ++ return; ++ } ++ // Folia end - region threading + this.teleportToAroundBlockPos(entityliving.blockPosition()); + } + +@@ -325,7 +330,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { + return false; + } + Location to = event.getTo(); +- this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick ++ // also, use teleportAsync so that crossing region boundaries will not blow up ++ Location finalTo = to; ++ Level sourceWorld = this.level(); ++ this.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { ++ if (nmsEntity.level() == sourceWorld) { ++ nmsEntity.teleportAsync( ++ (net.minecraft.server.level.ServerLevel)nmsEntity.level(), ++ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), ++ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), ++ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ } ++ }, null, 1L); ++ // Folia end - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick + // CraftBukkit end + this.navigation.stop(); + return true; +diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java +index af6f91c66e9cc7e0d491e6efed992a140947155e..d4e6198fdfbefe54e374479a1f1d835ab98ce93a 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/Brain.java ++++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java +@@ -424,9 +424,17 @@ public class Brain { + } + + public void stopAll(ServerLevel world, E entity) { ++ // Folia start - region threading ++ List> behaviors = this.getRunningBehaviors(); ++ if (behaviors.isEmpty()) { ++ // avoid calling getGameTime, as this may be called while portalling an entity - which will cause ++ // the world data retrieval to fail ++ return; ++ } ++ // Folia end - region threading + long l = entity.level().getGameTime(); + +- for (BehaviorControl behaviorControl : this.getRunningBehaviors()) { ++ for (BehaviorControl behaviorControl : behaviors) { // Folia - region threading + behaviorControl.doStop(world, entity, l); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +index 7302d397d39d8400527ab2da4adaf8d792256749..ee3b8de9b700202da776c68579532bf11319a001 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +@@ -19,6 +19,11 @@ public class PoiCompetitorScan { + context, + (jobSite, mobs) -> (world, entity, time) -> { + GlobalPos globalPos = context.get(jobSite); ++ // Folia start - region threading ++ if (globalPos.dimension() != world.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, globalPos.pos())) { ++ return true; ++ } ++ // Folia end - region threading + world.getPoiManager() + .getType(globalPos.pos()) + .ifPresent( +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +index 15d7be9ed4a973044dd4399db46aaa244730b836..df4cce1d3baef0ad386f5bc201663ae569e7b36d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +@@ -51,7 +51,7 @@ public class FollowOwnerGoal extends Goal { + public boolean canContinueToUse() { + return !this.navigation.isDone() + && !this.tamable.unableToMoveToOwner() +- && !(this.tamable.distanceToSqr(this.owner) <= (double)(this.stopDistance * this.stopDistance)); ++ && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= (double)(this.stopDistance * this.stopDistance)); // Folia - region threading - check level + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +index 2796df7af365c452b28373adfd7daf1d6730bac5..45b68a139956f2d59c8b9658692d01f1f79d33cc 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +@@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation { + + @Override + public Path createPath(BlockPos target, @javax.annotation.Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, target)) { ++ return null; ++ } ++ // Folia end - region threading + LevelChunk levelChunk = this.level + .getChunkSource() + .getChunkNow(SectionPos.blockToSectionCoord(target.getX()), SectionPos.blockToSectionCoord(target.getZ())); +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 48c0de870a5bbf647309e69361dfb10ab56c65ab..71885b8b3f491fbe644efef02aebdd8b9f03a10b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -96,11 +96,11 @@ public abstract class PathNavigation { + } + + public void recomputePath() { +- if (this.level.getGameTime() - this.timeLastRecompute > 20L) { ++ if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading + if (this.targetPos != null) { + this.path = null; + this.path = this.createPath(this.targetPos, this.reachRange); +- this.timeLastRecompute = this.level.getGameTime(); ++ this.timeLastRecompute = this.tick; // Folia - region threading + this.hasDelayedRecomputation = false; + } + } else { +@@ -220,7 +220,7 @@ public abstract class PathNavigation { + + public boolean moveTo(Entity entity, double speed) { + // Paper start - Perf: Optimise pathfinding +- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { ++ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading + return false; + } + // Paper end - Perf: Optimise pathfinding +@@ -232,7 +232,7 @@ public abstract class PathNavigation { + return true; + } else { + this.pathfindFailures++; +- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; ++ this.lastFailure = this.tick; // Folia - region threading + return false; + } + // Paper end - Perf: Optimise pathfinding +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +index e1ff702e56adef6c8a572b078b49de2143c4ce7e..6c9e9c4173e62fc69e6da2b0bc7e6cc886f39fbe 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +@@ -22,7 +22,7 @@ public class PlayerSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, LivingEntity entity) { +- List list = world.players() ++ List list = world.getLocalPlayers() // Folia - region threading + .stream() + .filter(EntitySelector.NO_SPECTATORS) + .filter(player -> entity.closerThan(player, this.getFollowDistance(entity))) +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +index 4090d98b264d13f76f93742bac7e895a5b0a72ea..4dd2dcb092ef6301d9986d4217d8ca647ec4565b 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +@@ -39,7 +39,7 @@ public class TemptingSensor extends Sensor { + protected void doTick(ServerLevel world, PathfinderMob entity) { + Brain behaviorcontroller = entity.getBrain(); + TargetingConditions pathfindertargetcondition = TemptingSensor.TEMPT_TARGETING.copy().range((double) ((float) entity.getAttributeValue(Attributes.TEMPT_RANGE))); +- Stream stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error ++ Stream stream = world.getLocalPlayers().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error // Folia - region threading + return pathfindertargetcondition.test(world, entity, entityplayer); + }).filter(this::playerHoldingTemptation).filter((entityplayer) -> { + return !entity.hasPassenger((Entity) entityplayer); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +index bd3f71c3eaa33258ff56062ea3a2099cef310b7a..326be10d31d0b546a325f4af97bb413910e79e31 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +@@ -21,62 +21,66 @@ import org.slf4j.Logger; + public class VillageSiege implements CustomSpawner { + + private static final Logger LOGGER = LogUtils.getLogger(); +- private boolean hasSetupSiege; +- private VillageSiege.State siegeState; +- private int zombiesToSpawn; +- private int nextSpawnTime; +- private int spawnX; +- private int spawnY; +- private int spawnZ; ++ // Folia - region threading + + public VillageSiege() { +- this.siegeState = VillageSiege.State.SIEGE_DONE; ++ // Folia - region threading + } + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ // Folia start - region threading ++ // check if the spawn pos is no longer owned by this region ++ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE ++ && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { ++ // can't spawn here, just re-set ++ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState(); ++ } ++ // Folia end - region threading + if (!world.isDay() && spawnMonsters) { + float f = world.getTimeOfDay(0.0F); + + if ((double) f == 0.5D) { +- this.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading + } + +- if (this.siegeState == VillageSiege.State.SIEGE_DONE) { ++ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading + return 0; + } else { +- if (!this.hasSetupSiege) { ++ if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading + if (!this.tryToSetupSiege(world)) { + return 0; + } + +- this.hasSetupSiege = true; ++ worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading + } + +- if (this.nextSpawnTime > 0) { +- --this.nextSpawnTime; ++ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading ++ --worldData.villageSiegeState.nextSpawnTime; // Folia - region threading + return 0; + } else { +- this.nextSpawnTime = 2; +- if (this.zombiesToSpawn > 0) { ++ worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading ++ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading + this.trySpawn(world); +- --this.zombiesToSpawn; ++ --worldData.villageSiegeState.zombiesToSpawn; // Folia - region threading + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading + } + + return 1; + } + } + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; +- this.hasSetupSiege = false; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading ++ worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading + return 0; + } + } + + private boolean tryToSetupSiege(ServerLevel world) { +- Iterator iterator = world.players().iterator(); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading + + while (iterator.hasNext()) { + Player entityhuman = (Player) iterator.next(); +@@ -88,12 +92,12 @@ public class VillageSiege implements CustomSpawner { + for (int i = 0; i < 10; ++i) { + float f = world.random.nextFloat() * 6.2831855F; + +- this.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); +- this.spawnY = blockposition.getY(); +- this.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); +- if (this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { +- this.nextSpawnTime = 0; +- this.zombiesToSpawn = 20; ++ worldData.villageSiegeState.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading ++ worldData.villageSiegeState.spawnY = blockposition.getY(); // Folia - region threading ++ worldData.villageSiegeState.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading ++ if (this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading ++ worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading ++ worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading + break; + } + } +@@ -107,13 +111,15 @@ public class VillageSiege implements CustomSpawner { + } + + private void trySpawn(ServerLevel world) { +- Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading + + if (vec3d != null) { + Zombie entityzombie; + + try { + entityzombie = new Zombie(world); ++ entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up + entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), EntitySpawnReason.EVENT, (SpawnGroupData) null); + } catch (Exception exception) { + VillageSiege.LOGGER.warn("Failed to create zombie for village siege at {}", vec3d, exception); +@@ -121,7 +127,7 @@ public class VillageSiege implements CustomSpawner { + return; + } + +- entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F); ++ // Folia - region threading - move up + world.addFreshEntityWithPassengers(entityzombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit + } + } +@@ -142,7 +148,7 @@ public class VillageSiege implements CustomSpawner { + return null; + } + +- private static enum State { ++ public static enum State { // Folia - region threading + + SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE; + +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 63a94b6068fdaef8bb26675c2927cb729ced1dac..f3f98e6276dda3bc4f290fc2d80569f7e1e7ef66 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -58,11 +58,13 @@ public class PoiManager extends SectionStorage im + } + + private void updateDistanceTracking(long section) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + if (this.isVillageCenter(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } ++ } // Folia - region threading + } + + @Override +@@ -345,10 +347,12 @@ public class PoiManager extends SectionStorage im + } + + public int sectionsToVillage(SectionPos pos) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + // Paper start - rewrite chunk system + this.villageDistanceTracker.propagateUpdates(); + return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); + // Paper end - rewrite chunk system ++ } // Folia - region threading + } + + boolean isVillageCenter(long pos) { +@@ -362,7 +366,9 @@ public class PoiManager extends SectionStorage im + + @Override + public void tick(BooleanSupplier shouldKeepTicking) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system ++ } // Folia - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java +index 0bafe14342c1acce131ad34717c18aed3718deed..63ee0cd25cf595196ad67d90737daf7826925b2a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java +@@ -1129,6 +1129,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + public boolean canBeeUse() { ++ // Folia start - region threading ++ if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) { ++ Bee.this.hivePos = null; ++ } ++ // Folia end - region threading + return Bee.this.hivePos != null && !Bee.this.isTooFarAway(Bee.this.hivePos) && !Bee.this.hasRestriction() && Bee.this.wantsToEnterHive() && !this.hasReachedTarget(Bee.this.hivePos) && Bee.this.level().getBlockState(Bee.this.hivePos).is(BlockTags.BEEHIVES); + } + +@@ -1242,6 +1247,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + @Override + public boolean canBeeUse() { ++ // Folia start - region threading ++ if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) { ++ Bee.this.savedFlowerPos = null; ++ } ++ // Folia end - region threading + return Bee.this.savedFlowerPos != null && !Bee.this.hasRestriction() && this.wantsToGoToKnownFlower() && !Bee.this.closerThan(Bee.this.savedFlowerPos, 2); + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java +index 989b7be74eaeba7f40eac87c7ee7f252cb0c05c9..d3110cbb22995fb197739cdbcf480f584507fc26 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java +@@ -365,7 +365,7 @@ public class Cat extends TamableAnimal implements VariantHolder 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot +- this.handlePortal(); ++ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading ++ //this.handlePortal(); // Folia - region threading + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); + this.applyEffectsFromBlocks(); +@@ -142,7 +142,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + */ + // Send position and velocity updates to nearby players on every tick while the TNT is in water. + // This does pretty well at keeping their clients in sync with the server. +- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); ++ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.moonrise$getTrackedEntity(); // Folia - region threading + if (ete != null) { + net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); + net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java +index 183a33b7d666d652b455baa7e8339e9c4a870a58..64af789f203275611e142955daef0a783ace7e26 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java +@@ -347,7 +347,7 @@ public class Vex extends Monster implements TraceableEntity { + public void tick() { + BlockPos blockposition = Vex.this.getBoundOrigin(); + +- if (blockposition == null) { ++ if (blockposition == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), blockposition)) { // Folia - region threading + blockposition = Vex.this.blockPosition(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +index 30bce56a70f923b0ec77c8e3f29e435a71c71510..ded64f670120c6079fac7f44e8870ca8f97353ea 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -76,7 +76,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + @Nullable + private MerchantOffers tradeOffers; + private int villagerXp; +- private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field ++ // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers + + public ZombieVillager(EntityType type, Level world) { + super(type, world); +@@ -157,10 +157,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + public void tick() { + if (!this.level().isClientSide && this.isAlive() && this.isConverting()) { + int i = this.getConversionProgress(); +- // CraftBukkit start - Use wall time instead of ticks for villager conversion +- int elapsedTicks = MinecraftServer.currentTick - this.lastTick; +- i *= elapsedTicks; +- // CraftBukkit end ++ // Folia - region threading - restore original timers + + this.villagerConversionTime -= i; + if (this.villagerConversionTime <= 0) { +@@ -169,7 +166,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + } + + super.tick(); +- this.lastTick = MinecraftServer.currentTick; // CraftBukkit ++ // Folia - region threading - restore original timers + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index 3c037cabd8331eb96a6523b37abab4e73ab79a02..a1dee26ed47a423fbad57b9b2ec98b4f58b9470b 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -224,10 +224,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + this.readInventoryFromTag(nbt, this.registryAccess()); + } + ++ // Folia start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopTrading(); ++ } ++ // Folia end - region threading ++ + @Nullable + @Override + public Entity teleport(TeleportTransition teleportTarget) { +- this.stopTrading(); ++ this.preChangeDimension(); // Folia - region threading - move into preChangeDimension + return super.teleport(teleportTarget); + } + +diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +index b0236c7bf9441aa84d3795ffed05dd6099f29636..4a8214160946f94d8a2fa13785f205a324a32949 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +@@ -18,17 +18,18 @@ import net.minecraft.world.phys.AABB; + + public class CatSpawner implements CustomSpawner { + private static final int TICK_DELAY = 1200; +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + if (spawnAnimals && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { +- this.nextTick--; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ --worldData.catSpawnerNextTick; // Folia - region threading ++ if (worldData.catSpawnerNextTick > 0) { // Folia - region threading + return 0; + } else { +- this.nextTick = 1200; +- Player player = world.getRandomPlayer(); ++ worldData.catSpawnerNextTick = 1200; // Folia - region threading ++ Player player = world.getRandomLocalPlayer(); // Folia - region threading + if (player == null) { + return 0; + } else { +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 2d8ba55906c8da16fde850e3412f4a6bda3d56e7..07f50048e9748b28178847ad470b8b2ce37e0eea 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -204,7 +204,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + brain.setCoreActivities(ImmutableSet.of(Activity.CORE)); + brain.setDefaultActivity(Activity.IDLE); + brain.setActiveActivityIfPossible(Activity.IDLE); +- brain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); ++ brain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading - not in the world yet + } + + @Override +@@ -710,6 +710,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension()); + + if (worldserver != null) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading ++ worldserver, globalpos.pos().getX() >> 4, globalpos.pos().getZ() >> 4, () -> { // Folia - region threading + PoiManager villageplace = worldserver.getPoiManager(); + Optional> optional = villageplace.getType(globalpos.pos()); + BiPredicate> bipredicate = (BiPredicate) Villager.POI_MEMORIES.get(pos); +@@ -718,6 +720,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + villageplace.release(globalpos.pos()); + DebugPackets.sendPoiTicketCountPacket(worldserver, globalpos.pos()); + } ++ }); // Folia - region threading + + } + }); +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +index a728dcbf956f108f01c966c7531449a506a14a87..d8e5ef40b32dd2e8b5fb6f4a00a3d3faea6e66ba 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -32,16 +32,14 @@ public class WanderingTraderSpawner implements CustomSpawner { + private static final int SPAWN_CHANCE_INCREASE = 25; + private static final int SPAWN_ONE_IN_X_CHANCE = 10; + private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; +- private final RandomSource random = RandomSource.create(); ++ private final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading + private final ServerLevelData serverLevelData; +- private int tickDelay; +- private int spawnDelay; +- private int spawnChance; ++ // Folia - region threading + + public WanderingTraderSpawner(ServerLevelData properties) { + this.serverLevelData = properties; + // Paper start - Add Wandering Trader spawn rate config options +- this.tickDelay = Integer.MIN_VALUE; ++ //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata + //this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value + //this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value + //if (this.spawnDelay == 0 && this.spawnChance == 0) { +@@ -56,36 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner { + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + // Paper start - Add Wandering Trader spawn rate config options +- if (this.tickDelay == Integer.MIN_VALUE) { +- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; +- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading ++ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading ++ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading ++ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading + } + if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { + return 0; +- } else if (this.tickDelay - 1 > 0) { +- this.tickDelay = this.tickDelay - 1; ++ } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading ++ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading + return 0; + } else { +- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; ++ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading ++ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways +- if (this.spawnDelay > 0) { ++ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading + return 0; + } else { +- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; ++ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading + if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { + return 0; + } else { +- int i = this.spawnChance; ++ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading + + // this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways +- this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); ++ worldData.wanderingTraderSpawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading + if (this.random.nextInt(100) > i) { + return 0; + } else if (this.spawn(world)) { +- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading + // Paper end - Add Wandering Trader spawn rate config options + return 1; + } else { +@@ -97,7 +96,7 @@ public class WanderingTraderSpawner implements CustomSpawner { + } + + private boolean spawn(ServerLevel world) { +- ServerPlayer entityplayer = world.getRandomPlayer(); ++ ServerPlayer entityplayer = world.getRandomLocalPlayer(); // Folia - region threading + + if (entityplayer == null) { + return true; +@@ -127,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner { + this.tryToSpawnLlamaFor(world, entityvillagertrader, 4); + } + +- this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); ++ //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... + // entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent + entityvillagertrader.setWanderTarget(blockposition1); + entityvillagertrader.restrictTo(blockposition1, 16); +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 5b8b85a295a08ae495f729c595b3a78778965342..f2817f1c29431ef2c4a45dc9ef90f06d4982f7c9 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -1518,6 +1518,14 @@ public abstract class Player extends LivingEntity { + + } + ++ // Folia start - region threading ++ @Override ++ protected void preRemove(RemovalReason reason) { ++ super.preRemove(reason); ++ this.fishing = null; ++ } ++ // Folia end - region threading ++ + public boolean isLocalPlayer() { + return false; + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +index f4513b1887f823b088dabe425be042b8fb2bde66..5d52f67b3e379e64d23a0429679c144373d20eac 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -198,6 +198,11 @@ public abstract class AbstractArrow extends Projectile { + + @Override + public void tick() { ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + boolean flag = !this.isNoPhysics(); + Vec3 vec3d = this.getDeltaMovement(); + BlockPos blockposition = this.blockPosition(); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +index 19ff16e1ba406584f3cdd760d0269a50980b0a26..3e059ce6b4e7b730d0e3460dc76591ea0c69cfe1 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +@@ -89,6 +89,11 @@ public abstract class AbstractHurtingProjectile extends Projectile { + this.setPos(vec3d); + this.applyEffectsFromBlocks(); + super.tick(); ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + if (this.shouldBurn()) { + this.igniteForSeconds(1.0F); + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +index 8a4e7e1c0c4919d2ee34121c14f9665b9ad95273..e6dedd9e1421515459dfc156007ecff5cba1faa5 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +@@ -144,6 +144,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + + }); + } ++ // Folia start - region threading ++ if (this.attachedToEntity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.attachedToEntity)) { ++ this.attachedToEntity = null; ++ } ++ // Folia end - region threading + + if (this.attachedToEntity != null) { + if (this.attachedToEntity.isFallFlying()) { +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +index 5e6ceb3c3728c0c08a516566c70a5c0d72d59196..50ab937bdbe444331fda91bf3bae99f0accaf735 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +@@ -105,7 +105,7 @@ public class FishingHook extends Projectile { + + public FishingHook(net.minecraft.world.entity.player.Player thrower, Level world, int luckBonus, int waitTimeReductionTicks) { + this(EntityType.FISHING_BOBBER, world, luckBonus, waitTimeReductionTicks); +- this.setOwner(thrower); ++ //this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail + float f = thrower.getXRot(); + float f1 = thrower.getYRot(); + float f2 = Mth.cos(-f1 * 0.017453292F - 3.1415927F); +@@ -117,6 +117,7 @@ public class FishingHook extends Projectile { + double d2 = thrower.getZ() - (double) f2 * 0.3D; + + this.moveTo(d0, d1, d2, f1, f); ++ this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail + Vec3 vec3d = new Vec3((double) (-f3), (double) Mth.clamp(-(f5 / f4), -5.0F, 5.0F), (double) (-f2)); + double d3 = vec3d.length(); + +@@ -273,6 +274,11 @@ public class FishingHook extends Projectile { + } + + private boolean shouldStopFishing(net.minecraft.world.entity.player.Player player) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { ++ return true; ++ } ++ // Folia end - region threading + ItemStack itemstack = player.getMainHandItem(); + ItemStack itemstack1 = player.getOffhandItem(); + boolean flag = itemstack.is(Items.FISHING_ROD); +@@ -636,10 +642,18 @@ public class FishingHook extends Projectile { + @Override + public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { + // CraftBukkit end +- this.updateOwnerInfo((FishingHook) null); ++ // Folia - region threading - move into preRemove + super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause + } + ++ // Folia start - region threading ++ @Override ++ protected void preRemove(RemovalReason reason) { ++ super.preRemove(reason); ++ this.updateOwnerInfo((FishingHook) null); ++ } ++ // Folia end - region threading ++ + @Override + public void onClientRemoval() { + this.updateOwnerInfo((FishingHook) null); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +index 958ea103cc80da7366cc33dc385b76d4f5c809f2..40645ef84efb096646d7bf7cd8c445b981c15de6 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +@@ -41,6 +41,11 @@ public class LlamaSpit extends Projectile { + @Override + public void tick() { + super.tick(); ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + Vec3 vec3d = this.getDeltaMovement(); + HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index 9a7b56b653848974e1194eb4f6d40cb99a96ff57..a6bcf7b57b804af74f75c0b24ff48ee2714c3b73 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -44,7 +44,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { + @Nullable + public UUID ownerUUID; + @Nullable +- public Entity cachedOwner; ++ public org.bukkit.craftbukkit.entity.CraftEntity cachedOwner; // Folia - region threading - replace with CraftEntity + public boolean leftOwner; + public boolean hasBeenShot; + @Nullable +@@ -61,7 +61,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { + public void setOwner(@Nullable Entity entity) { + if (entity != null) { + this.ownerUUID = entity.getUUID(); +- this.cachedOwner = entity; ++ this.cachedOwner = entity.getBukkitEntity(); // Folia - region threading + } + // Paper start - Refresh ProjectileSource for projectiles + else { +@@ -77,22 +77,38 @@ public abstract class Projectile extends Entity implements TraceableEntity { + if (fillCache) { + this.getOwner(); + } +- if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof ProjectileSource projSource) { ++ if (this.cachedOwner != null && !this.cachedOwner.getHandleRaw().isRemoved() && this.projectileSource == null && this.cachedOwner instanceof ProjectileSource projSource) { // Folia - region threading + this.projectileSource = projSource; + } + } + // Paper end - Refresh ProjectileSource for projectiles + ++ // Folia start - region threading ++ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. ++ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. + @Nullable + @Override + public Entity getOwner() { +- if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { ++ Entity ret = this.getOwnerRaw(); ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? ret : null; ++ } ++ // Folia end - region threading ++ ++ @Nullable ++ public Entity getOwnerRaw() { // Folia - region threading ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading ++ if (this.cachedOwner != null && !this.cachedOwner.isPurged()) { // Folia - region threading + this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles +- return this.cachedOwner; ++ return this.cachedOwner.getHandleRaw(); // Folia - region threading + } else if (this.ownerUUID != null) { +- this.cachedOwner = this.findOwner(this.ownerUUID); ++ // Folia start - region threading ++ Entity ret = this.findOwner(this.ownerUUID); ++ if (ret != null) { ++ this.cachedOwner = ret.getBukkitEntity(); ++ } ++ // Folia end - region threading + this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles +- return this.cachedOwner; ++ return ret; // Folia - region threading + } else { + return null; + } +@@ -144,7 +160,12 @@ public abstract class Projectile extends Entity implements TraceableEntity { + protected void setOwnerThroughUUID(UUID uuid) { + if (this.ownerUUID != uuid) { + this.ownerUUID = uuid; +- this.cachedOwner = this.findOwner(uuid); ++ // Folia start - region threading ++ Entity cachedOwner = this.findOwner(this.ownerUUID); ++ if (cachedOwner != null) { ++ this.cachedOwner = cachedOwner.getBukkitEntity(); ++ } ++ // Folia end - region threading + } + + } +@@ -468,7 +489,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { + public boolean mayInteract(ServerLevel world, BlockPos pos) { + Entity entity = this.getOwner(); + +- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return entity instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading + } + + public boolean mayBreak(ServerLevel world) { +diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +index bb159ea4baf208aab6d6fcfbbddacd5b089b55c8..6a0c3e8e304f3861366e06345b20bd790d858e55 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -29,7 +29,7 @@ public class SmallFireball extends Fireball { + public SmallFireball(Level world, LivingEntity owner, Vec3 velocity) { + super(EntityType.SMALL_FIREBALL, owner, velocity, world); + // CraftBukkit start +- if (this.getOwner() != null && this.getOwner() instanceof Mob) { ++ if (owner != null && this.getOwner() instanceof Mob) { // Folia - region threading + this.isIncendiary = (world instanceof ServerLevel worldserver) && worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java +index c309198d092fdae6bdcc5d773b7b707bab2738bd..ff53dfec183e7933e5ceda8bf5a88b179d8d8532 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java +@@ -46,6 +46,11 @@ public abstract class ThrowableProjectile extends Projectile { + + @Override + public void tick() { ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + this.handleFirstTickBubbleColumn(); + this.applyGravity(); + this.applyInertia(); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index bd2684528157f928460f2143dd71a48e11983123..3bd0f3ae53eaa22409152d7f41e511e76bdaa265 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -120,6 +120,81 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); + } + ++ // Folia start - region threading ++ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { ++ final boolean onPortalCooldown = source.isOnPortalCooldown(); ++ // ignore retired callback, in those cases we do not want to teleport ++ source.getBukkitEntity().taskScheduler.schedule( ++ (Entity entity) -> { ++ if (!isAllowedToTeleportOwner(entity, checkWorld)) { ++ return; ++ } ++ // source is now an invalid reference, do not use it, use the entity parameter ++ net.minecraft.world.phys.Vec3 endermitePos = entity.position(); ++ ++ // dismount from any vehicles, so we can teleport and to prevent desync ++ if (entity.isPassenger()) { ++ entity.unRide(); ++ } ++ ++ if (onPortalCooldown) { ++ entity.setPortalCooldown(); ++ } ++ ++ entity.teleportAsync( ++ checkWorld, to, null, null, null, ++ PlayerTeleportEvent.TeleportCause.ENDER_PEARL, ++ // chunk could have been unloaded ++ Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ (Entity teleported) -> { ++ // entity is now an invalid reference, do not use it, instead use teleported ++ if (teleported instanceof ServerPlayer player) { ++ // connection teleport is already done ++ ServerLevel world = player.serverLevel(); ++ ++ // endermite spawn chance ++ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world, EntitySpawnReason.TRIGGERED); ++ ++ if (entityendermite != null) { ++ float yRot = teleported.getYRot(); ++ float xRot = teleported.getXRot(); ++ Runnable spawn = () -> { ++ entityendermite.moveTo(endermitePos.x, endermitePos.y, endermitePos.z, yRot, xRot); ++ world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); ++ }; ++ ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, endermitePos, net.minecraft.world.phys.Vec3.ZERO, 1)) { ++ spawn.run(); ++ } else { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.x), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.z), ++ spawn ++ ); ++ } ++ } ++ } ++ ++ // damage player ++ teleported.resetFallDistance(); ++ player.resetCurrentImpulseContext(); ++ player.hurtServer(player.serverLevel(), player.damageSources().enderPearl().customEventDamager(player), 5.0F); // CraftBukkit // Paper - fix DamageSource API ++ playSound(teleported.level(), to); ++ } else { ++ // reset fall damage so that if the entity was falling they do not instantly die ++ teleported.resetFallDistance(); ++ playSound(teleported.level(), to); ++ } ++ } ++ ); ++ }, ++ null, 1L ++ ); ++ } ++ // Folia end - region threading ++ + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); +@@ -132,6 +207,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + if (world instanceof ServerLevel worldserver) { + if (!this.isRemoved()) { ++ // Folia start - region threading ++ if (true) { ++ // we can't fire events, because we do not actually know where the other entity is located ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) { ++ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); ++ } ++ Entity entity = this.getOwnerRaw(); ++ if (entity != null) { ++ attemptTeleport(entity, (ServerLevel)this.level(), this.position()); ++ } ++ this.discard(EntityRemoveEvent.Cause.HIT); ++ return; ++ } ++ // Folia end - region threading + Entity entity = this.getOwner(); + + if (entity != null && ThrownEnderpearl.isAllowedToTeleportOwner(entity, worldserver)) { +@@ -242,7 +331,15 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + } + } + +- private void playSound(Level world, Vec3 pos) { ++ // Folia start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ // Don't change the owner here, since the tick logic will consider it anyways. ++ } ++ // Folia end - region threading ++ ++ private static void playSound(Level world, Vec3 pos) { // Folia - region threading - static + world.playSound((Player) null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); + } + +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java +index 11cf2d9def087b0898c828eaa21eb5f7b8811d5f..5ff4a4af7f166f5f977efe41263ca487fe1b270b 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java +@@ -113,6 +113,13 @@ public class Raid { + public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); + // Paper end + ++ // Folia start - make raids thread-safe ++ public boolean ownsRaid() { ++ BlockPos center = this.getCenter(); ++ return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8); ++ } ++ // Folia end - make raids thread-safe ++ + public Raid(int id, ServerLevel world, BlockPos pos) { + this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10); + this.random = RandomSource.create(); +@@ -226,7 +233,7 @@ public class Raid { + return (entityplayer) -> { + BlockPos blockposition = entityplayer.blockPosition(); + +- return entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityplayer) && entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; // Folia - make raids thread-safe + }; + } + +@@ -541,7 +548,7 @@ public class Raid { + boolean flag = true; + Collection collection = this.raidEvent.getPlayers(); + long i = this.random.nextLong(); +- Iterator iterator = this.level.players().iterator(); ++ Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java +index cee1e4db2312efb4843c4b6dc18f4af10b91d304..d833c92018895d0576ba08578be7b1c6c49ac1b2 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java +@@ -92,7 +92,7 @@ public abstract class Raider extends PatrollingMonster { + + if (this.canJoinRaid()) { + if (raid == null) { +- if (this.level().getGameTime() % 20L == 0L) { ++ if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + Raid raid1 = ((ServerLevel) this.level()).getRaidAt(this.blockPosition()); + + if (raid1 != null && Raids.canJoinRaid(this, raid1)) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java +index 439d61d8689fabe940006b9b317a6810175dccfb..81ac4b7df5c402a618f6d7196b905ea065d6e822 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java +@@ -26,9 +26,9 @@ import net.minecraft.world.phys.Vec3; + public class Raids extends SavedData { + + private static final String RAID_FILE_ID = "raids"; +- public final Map raidMap = Maps.newHashMap(); ++ public final Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe + private final ServerLevel level; +- private int nextAvailableID; ++ private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe + private int tick; + + public static SavedData.Factory factory(ServerLevel world) { +@@ -41,7 +41,7 @@ public class Raids extends SavedData { + + public Raids(ServerLevel world) { + this.level = world; +- this.nextAvailableID = 1; ++ this.nextAvailableID.set(1); // Folia - make raids thread-safe + this.setDirty(); + } + +@@ -49,12 +49,26 @@ public class Raids extends SavedData { + return (Raid) this.raidMap.get(id); + } + +- public void tick() { ++ // Folia start - make raids thread-safe ++ public void globalTick() { + ++this.tick; ++ if (this.tick % 200 == 0) { ++ this.setDirty(); ++ } ++ // Folia end - make raids thread-safe ++ } ++ ++ public void tick() { ++ // Folia - make raids thread-safe - move to globalTick() + Iterator iterator = this.raidMap.values().iterator(); + + while (iterator.hasNext()) { + Raid raid = (Raid) iterator.next(); ++ // Folia start - make raids thread-safe ++ if (!raid.ownsRaid()) { ++ continue; ++ } ++ // Folia end - make raids thread-safe + + if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { + raid.stop(); +@@ -68,14 +82,17 @@ public class Raids extends SavedData { + } + } + +- if (this.tick % 200 == 0) { +- this.setDirty(); +- } ++ // Folia - make raids thread-safe - move to globalTick() + + DebugPackets.sendRaids(this.level, this.raidMap.values()); + } + + public static boolean canJoinRaid(Raider raider, Raid raid) { ++ // Folia start - make raids thread-safe ++ if (!raid.ownsRaid()) { ++ return false; ++ } ++ // Folia end - make raids thread-safe + return raider != null && raid != null && raid.getLevel() != null ? raider.isAlive() && raider.canJoinRaid() && raider.getNoActionTime() <= 2400 && raider.level().dimensionType() == raid.getLevel().dimensionType() : false; + } + +@@ -88,7 +105,7 @@ public class Raids extends SavedData { + } else { + DimensionType dimensionmanager = player.level().dimensionType(); + +- if (!dimensionmanager.hasRaids()) { ++ if (!dimensionmanager.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, pos.getX() >> 4, pos.getZ() >> 4, 8) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading + return null; + } else { + List list = this.level.getPoiManager().getInRange((holder) -> { +@@ -150,7 +167,7 @@ public class Raids extends SavedData { + public static Raids load(ServerLevel world, CompoundTag nbt) { + Raids persistentraid = new Raids(world); + +- persistentraid.nextAvailableID = nbt.getInt("NextAvailableID"); ++ persistentraid.nextAvailableID.set(nbt.getInt("NextAvailableID")); // Folia - make raids thread-safe + persistentraid.tick = nbt.getInt("Tick"); + ListTag nbttaglist = nbt.getList("Raids", 10); + +@@ -166,7 +183,7 @@ public class Raids extends SavedData { + + @Override + public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries) { +- nbt.putInt("NextAvailableID", this.nextAvailableID); ++ nbt.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe + nbt.putInt("Tick", this.tick); + ListTag nbttaglist = new ListTag(); + Iterator iterator = this.raidMap.values().iterator(); +@@ -188,7 +205,7 @@ public class Raids extends SavedData { + } + + private int getUniqueId() { +- return ++this.nextAvailableID; ++ return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe + } + + @Nullable +@@ -199,6 +216,11 @@ public class Raids extends SavedData { + + while (iterator.hasNext()) { + Raid raid1 = (Raid) iterator.next(); ++ // Folia start - make raids thread-safe ++ if (!raid1.ownsRaid()) { ++ continue; ++ } ++ // Folia end - make raids thread-safe + double d1 = raid1.getCenter().distSqr(pos); + + if (raid1.isActive() && d1 < d0) { +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +index a811cdca34bb468ac355f72decd153d4f0618009..ef33d3cba37e2b2b70650af55841c21788699123 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +@@ -142,5 +142,11 @@ public class MinecartCommandBlock extends AbstractMinecart { + return (org.bukkit.craftbukkit.entity.CraftMinecartCommand) MinecartCommandBlock.this.getBukkitEntity(); + } + // CraftBukkit end ++ // Folia start ++ @Override ++ public void threadCheck() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block"); ++ } ++ // Folia end + } + } +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 d81a6874e8b25f098df619f84c359e146c7f64de..e56d580a89931aba43602a758fad803fbe45f25e 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +@@ -145,7 +145,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper + + // Paper start + public void immunize() { +- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); + } + // Paper end + +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index f29415b8dff7d17328e159b56ae4ad46452f018e..b4bf2069e78cb1408593ed41575ee747651e33c8 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -429,31 +429,32 @@ public final class ItemStack implements DataComponentHolder { + DataComponentPatch oldData = this.components.asPatch(); + int oldCount = this.getCount(); + ServerLevel world = (ServerLevel) context.getLevel(); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + + if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement +- world.captureBlockStates = true; ++ worldData.captureBlockStates = true; // Folia - region threading + // special case bonemeal + if (item == Items.BONE_MEAL) { +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Folia - region threading + } + } + InteractionResult enuminteractionresult; + try { + enuminteractionresult = item.useOn(context); + } finally { +- world.captureBlockStates = false; ++ worldData.captureBlockStates = false; // Folia - region threading + } + DataComponentPatch newData = this.components.asPatch(); + int newCount = this.getCount(); + this.setCount(oldCount); + this.restorePatch(oldData); +- if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) { +- world.captureTreeGeneration = false; ++ if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Folia - region threading ++ worldData.captureTreeGeneration = false; + Location location = CraftLocation.toBukkit(blockposition, world.getWorld()); +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; +- List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ SaplingBlock.treeTypeRT.set(null); // Folia - region threading ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + StructureGrowEvent structureEvent = null; + if (treeType != null) { + boolean isBonemeal = this.getItem() == Items.BONE_MEAL; +@@ -479,10 +480,10 @@ public final class ItemStack implements DataComponentHolder { + entityhuman.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat + } + +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return ++ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading + return enuminteractionresult; + } +- world.captureTreeGeneration = false; ++ worldData.captureTreeGeneration = false; // Folia - region threading + + if (entityhuman != null && enuminteractionresult instanceof InteractionResult.Success) { + InteractionResult.Success enuminteractionresult_d = (InteractionResult.Success) enuminteractionresult; +@@ -490,8 +491,8 @@ public final class ItemStack implements DataComponentHolder { + if (enuminteractionresult_d.wasItemInteraction()) { + InteractionHand enumhand = context.getHand(); + org.bukkit.event.block.BlockPlaceEvent placeEvent = null; +- List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + if (blocks.size() > 1) { + placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(world, entityhuman, enumhand, blocks, blockposition.getX(), blockposition.getY(), blockposition.getZ()); + } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement +@@ -502,15 +503,15 @@ public final class ItemStack implements DataComponentHolder { + enuminteractionresult = InteractionResult.FAIL; // cancel placement + // PAIL: Remove this when MC-99075 fixed + placeEvent.getPlayer().updateInventory(); +- world.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot ++ worldData.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot // Folia - region threading + // revert back all captured blocks +- world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 +- world.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent ++ worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading ++ worldData.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading + for (BlockState blockstate : blocks) { + blockstate.update(true, false); + } +- world.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent +- world.preventPoiUpdated = false; ++ worldData.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading ++ worldData.preventPoiUpdated = false; // Folia - region threading + + // Brute force all possible updates + // Paper start - Don't resync blocks +@@ -519,7 +520,7 @@ public final class ItemStack implements DataComponentHolder { + // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); + // } + // Paper end - Don't resync blocks +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return ++ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading + } else { + // Change the stack to its new contents if it hasn't been tampered with. + if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), oldData)) { +@@ -527,7 +528,7 @@ public final class ItemStack implements DataComponentHolder { + this.setCount(newCount); + } + +- for (Map.Entry e : world.capturedTileEntities.entrySet()) { ++ for (Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading + world.setBlockEntity(e.getValue()); + } + +@@ -562,15 +563,15 @@ public final class ItemStack implements DataComponentHolder { + } + + // SPIGOT-4678 +- if (this.item instanceof SignItem && SignItem.openSign != null) { ++ if (this.item instanceof SignItem && SignItem.openSign.get() != null) { // Folia - region threading + try { +- if (world.getBlockEntity(SignItem.openSign) instanceof SignBlockEntity tileentitysign) { +- if (world.getBlockState(SignItem.openSign).getBlock() instanceof SignBlock blocksign) { ++ if (world.getBlockEntity(SignItem.openSign.get()) instanceof SignBlockEntity tileentitysign) { // Folia - region threading ++ if (world.getBlockState(SignItem.openSign.get()).getBlock() instanceof SignBlock blocksign) { // Folia - region threading + blocksign.openTextEdit(entityhuman, tileentitysign, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // Craftbukkit // Paper - Add PlayerOpenSignEvent + } + } + } finally { +- SignItem.openSign = null; ++ SignItem.openSign.set(null); // Folia - region threading + } + } + +@@ -599,8 +600,8 @@ public final class ItemStack implements DataComponentHolder { + } + } + } +- world.capturedTileEntities.clear(); +- world.capturedBlockStates.clear(); ++ worldData.capturedTileEntities.clear(); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + // CraftBukkit end + + return enuminteractionresult; +diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java +index 8ff50a4c7461bbd9f469d503f6b5ee482d2463d7..65d1af43b8bc740a9dc77dd8d5ba99de000fe57a 100644 +--- a/src/main/java/net/minecraft/world/item/MapItem.java ++++ b/src/main/java/net/minecraft/world/item/MapItem.java +@@ -68,6 +68,7 @@ public class MapItem extends Item { + } + + public void update(Level world, Entity entity, MapItemSavedData state) { ++ synchronized (state) { // Folia - make map data thread-safe + if (world.dimension() == state.dimension && entity instanceof Player) { + int i = 1 << state.scale; + int j = state.centerX; +@@ -97,8 +98,8 @@ public class MapItem extends Item { + int r = (j / i + o - 64) * i; + int s = (k / i + p - 64) * i; + Multiset multiset = LinkedHashMultiset.create(); +- LevelChunk levelChunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s)); // Paper - Maps shouldn't load chunks +- if (levelChunk != null && !levelChunk.isEmpty()) { // Paper - Maps shouldn't load chunks ++ LevelChunk levelChunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded ++ if (levelChunk != null && !levelChunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)world, levelChunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned + int t = 0; + double e = 0.0; + if (world.dimensionType().hasCeiling()) { +@@ -180,6 +181,7 @@ public class MapItem extends Item { + } + } + } ++ } // Folia - make map data thread-safe + } + + private BlockState getCorrectStateForFluidBlock(Level world, BlockState state, BlockPos pos) { +@@ -194,6 +196,7 @@ public class MapItem extends Item { + public static void renderBiomePreviewMap(ServerLevel world, ItemStack map) { + MapItemSavedData mapItemSavedData = getSavedData(map, world); + if (mapItemSavedData != null) { ++ synchronized (mapItemSavedData) { // Folia - make map data thread-safe + if (world.dimension() == mapItemSavedData.dimension) { + int i = 1 << mapItemSavedData.scale; + int j = mapItemSavedData.centerX; +@@ -263,6 +266,7 @@ public class MapItem extends Item { + } + } + } ++ } // Folia - make map data thread-safe + } + } + +@@ -271,6 +275,7 @@ public class MapItem extends Item { + if (!world.isClientSide) { + MapItemSavedData mapItemSavedData = getSavedData(stack, world); + if (mapItemSavedData != null) { ++ synchronized (mapItemSavedData) { // Folia - region threading + if (entity instanceof Player player) { + mapItemSavedData.tickCarriedBy(player, stack); + } +@@ -278,6 +283,7 @@ public class MapItem extends Item { + if (!mapItemSavedData.locked && (selected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) { + this.update(world, entity, mapItemSavedData); + } ++ } // Folia - region threading + } + } + } +diff --git a/src/main/java/net/minecraft/world/item/SignItem.java b/src/main/java/net/minecraft/world/item/SignItem.java +index fcc78491cef1cf1535888c4ed43c3b71fb597848..d6ff4ad0b9fbba491e6d0c96aaa62a2eaca89fd6 100644 +--- a/src/main/java/net/minecraft/world/item/SignItem.java ++++ b/src/main/java/net/minecraft/world/item/SignItem.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; + + public class SignItem extends StandingAndWallBlockItem { + +- public static BlockPos openSign; // CraftBukkit ++ public static final ThreadLocal openSign = new ThreadLocal<>(); // CraftBukkit // Folia - region threading + + public SignItem(Block standingBlock, Block wallBlock, Item.Properties settings) { + super(standingBlock, wallBlock, Direction.DOWN, settings); +@@ -39,7 +39,7 @@ public class SignItem extends StandingAndWallBlockItem { + + // CraftBukkit start - SPIGOT-4678 + // blocksign.openTextEdit(entityhuman, tileentitysign, true); +- SignItem.openSign = pos; ++ SignItem.openSign.set(pos); // Folia - region threading + // CraftBukkit end + } + } +diff --git a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +index 5cb39f95bd2d45b6c18554605f01d2ebf6473428..ccf476d1ed22cf992e3cbca6a375d36f85a82fa8 100644 +--- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java ++++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +@@ -22,7 +22,7 @@ import net.minecraft.world.phys.Vec3; + + public abstract class BaseCommandBlock implements CommandSource { + +- private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); ++ private static final ThreadLocal TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe + private static final Component DEFAULT_NAME = Component.literal("@"); + private long lastExecution = -1L; + private boolean updateLastExecution = true; +@@ -117,6 +117,7 @@ public abstract class BaseCommandBlock implements CommandSource { + } + + public boolean performCommand(Level world) { ++ if (true) return false; // Folia - region threading + if (!world.isClientSide && world.getGameTime() != this.lastExecution) { + if ("Searge".equalsIgnoreCase(this.command)) { + this.lastOutput = Component.literal("#itzlipofutzli"); +@@ -175,11 +176,14 @@ public abstract class BaseCommandBlock implements CommandSource { + this.customName = customName; + } + ++ public void threadCheck() {} // Folia ++ + @Override + public void sendSystemMessage(Component message) { + if (this.trackOutput) { + org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks +- SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT; ++ this.threadCheck(); // Folia ++ SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT.get(); // Folia - region threading - SDF is not thread-safe + Date date = new Date(); + + this.lastOutput = Component.literal("[" + simpledateformat.format(date) + "] ").append(message); +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 5d7a6e4b73f032db356e7ec369b150013e940ee6..6e895ca2844fd2d8189a6cf173d0c5dc9aaf02a9 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -24,6 +24,12 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + return this.getEntities(EntityTypeTest.forClass(entityClass), box, predicate); + } + ++ // Folia start - region threading ++ default List getLocalPlayers() { ++ return java.util.Collections.emptyList(); ++ } ++ // Folia end - region threading ++ + List players(); + + default List getEntities(@Nullable Entity except, AABB box) { +@@ -122,7 +128,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + double d = -1.0; + Player player = null; + +- for (Player player2 : this.players()) { ++ for (Player player2 : this.getLocalPlayers()) { // Folia - region threading + if (targetPredicate == null || targetPredicate.test(player2)) { + double e = player2.distanceToSqr(x, y, z); + if ((maxDistance < 0.0 || e < maxDistance * maxDistance) && (d == -1.0 || e < d)) { +@@ -143,7 +149,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { + com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); + +- for (Player human : this.players()) { ++ for (Player human : this.getLocalPlayers()) { // Folia - region threading + if (predicate == null || predicate.test(human)) { + double distanceSquared = human.distanceToSqr(x, y, z); + +@@ -170,7 +176,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + + // Paper start - Affects Spawning API + default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check + double distanceSqr = player.distanceToSqr(x, y, z); + if (range < 0.0D || distanceSqr < range * range) { +@@ -183,7 +189,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + // Paper end - Affects Spawning API + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { + double d = player.distanceToSqr(x, y, z); + if (range < 0.0 || d < range * range) { +@@ -197,8 +203,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst + + @Nullable + default Player getPlayerByUUID(UUID uuid) { +- for (int i = 0; i < this.players().size(); i++) { +- Player player = this.players().get(i); ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (uuid.equals(player.getUUID())) { + return player; + } +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 952c866bed29b412a48332ba1d98515f0f069781..68710af738fe6de115b75135f0f301763cb63b33 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -17,10 +17,11 @@ public interface Explosion { + @Nullable + static LivingEntity getIndirectSourceEntity(@Nullable Entity entity) { + return switch (entity) { +- case null, default -> null; ++ // Folia - decompile error - move down + case PrimedTnt primedTnt -> primedTnt.getOwner(); + case LivingEntity livingEntity -> livingEntity; + case Projectile projectile when projectile.getOwner() instanceof LivingEntity livingEntity2 -> livingEntity2; ++ case null, default -> null; // Folia - decompile error - moved down + }; + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 27f9d167b5ae9ce5117798ea44324107df59425f..fc180c3f68a40e909f7e357a6fcd06858b870e97 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -119,10 +119,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public static final int TICKS_PER_DAY = 24000; + public static final int MAX_ENTITY_SPAWN_Y = 20000000; + public static final int MIN_ENTITY_SPAWN_Y = -20000000; +- public final List blockEntityTickers = Lists.newArrayList(); // Paper - public +- protected final NeighborUpdater neighborUpdater; +- private final List pendingBlockEntityTickers = Lists.newArrayList(); +- private boolean tickingBlockEntities; ++ //public final List blockEntityTickers = Lists.newArrayList(); // Paper - public // Folia - region threading ++ public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; // Folia - region threading ++ //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading ++ //private boolean tickingBlockEntities; // Folia - region threading + public final Thread thread; + private final boolean isDebug; + private int skyDarken; +@@ -132,7 +132,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public float rainLevel; + protected float oThunderLevel; + public float thunderLevel; +- public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random ++ public final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading + /** @deprecated */ + @Deprecated + private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); +@@ -144,28 +144,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + private final ResourceKey dimension; + private final RegistryAccess registryAccess; + private final DamageSources damageSources; +- private long subTickCount; ++ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading + + // CraftBukkit start Added the following + private final CraftWorld world; + public boolean pvpMode; + public org.bukkit.generator.ChunkGenerator generator; + +- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 +- public boolean captureBlockStates = false; +- public boolean captureTreeGeneration = false; +- public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent +- public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper +- public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates +- public List captureDrops; ++ // Folia - region threading - moved to regionised data + 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; ++ // Folia - region threading - moved to regionised data ++ // Folia - region threading + public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot + // Paper start - add paper world config + private final io.papermc.paper.configuration.WorldConfiguration paperConfig; +@@ -178,9 +167,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; + private org.spigotmc.TickLimiter tileLimiter; +- private int tileTickPosition; +- public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions +- public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here ++ //private int tileTickPosition; // Folia - region threading ++ //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading ++ //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here // Folia - region threading + + public CraftWorld getWorld() { + return this.world; +@@ -830,6 +819,32 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); + } + // Paper end - optimise random ticking ++ // Folia start - region ticking ++ public final io.papermc.paper.threadedregions.RegionizedData worldRegionData ++ = new io.papermc.paper.threadedregions.RegionizedData<>( ++ (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this), ++ io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK ++ ); ++ public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData; ++ public final java.util.concurrent.ConcurrentHashMap.KeySetView needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet(); ++ ++ public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() { ++ final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ if (ret == null) { ++ return ret; ++ } ++ Level world = ret.world; ++ if (world != this) { ++ throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName()); ++ } ++ return ret; ++ } ++ ++ @Override ++ public List getLocalPlayers() { ++ return this.getCurrentWorldData().getLocalPlayers(); ++ } ++ // Folia end - region ticking + + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray + // Paper start - getblock optimisations - cache world height/sections +@@ -879,7 +894,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + this.thread = Thread.currentThread(); + this.biomeManager = new BiomeManager(this, i); + this.isDebug = flag1; +- this.neighborUpdater = new CollectingNeighborUpdater(this, j); ++ this.neighbourUpdateMax = j; // Folia - region threading + this.registryAccess = iregistrycustom; + this.damageSources = new DamageSources(iregistrycustom); + // CraftBukkit start +@@ -1024,8 +1039,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + @Nullable + public final BlockState getBlockStateIfLoaded(BlockPos pos) { + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -1087,16 +1102,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { ++ if (worldData.captureTreeGeneration) { // Folia - region threading + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed + BlockState type = getBlockState(pos); + if (!type.isDestroyable()) return false; + // Paper end - Protect Bedrock and End Portal/Frames from being destroyed +- CraftBlockState blockstate = this.capturedBlockStates.get(pos); ++ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading + if (blockstate == null) { + blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading + } + blockstate.setData(state); + blockstate.setFlag(flags); +@@ -1113,10 +1130,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + // CraftBukkit start - capture blockstates + boolean captured = false; +- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { ++ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading + CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot + blockstate.setFlag(flags); // Paper - set flag +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading + captured = true; + } + // CraftBukkit end +@@ -1126,8 +1143,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + if (iblockdata1 == null) { + // CraftBukkit start - remove blockstate if failed (or the same) +- if (this.captureBlockStates && captured) { +- this.capturedBlockStates.remove(pos); ++ if (worldData.captureBlockStates && captured) { // Folia - region threading ++ worldData.capturedBlockStates.remove(pos); // Folia - region threading + } + // CraftBukkit end + return false; +@@ -1164,7 +1181,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + */ + + // CraftBukkit start +- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates ++ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading + // Modularize client and physic updates + // Spigot start + try { +@@ -1209,7 +1226,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam + CraftWorld world = ((ServerLevel) this).getWorld(); + boolean cancelledUpdates = false; // Paper - Fix block place logic +- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent ++ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading + BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); + this.getCraftServer().getPluginManager().callEvent(event); + +@@ -1223,7 +1240,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + + // CraftBukkit start - SPIGOT-5710 +- if (!this.preventPoiUpdated) { ++ if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading + this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); + } + // CraftBukkit end +@@ -1309,7 +1326,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public void neighborShapeChanged(Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, int flags, int maxUpdateDepth) { +- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); ++ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); // Folia - region threading + } + + @Override +@@ -1334,11 +1351,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + return this.getChunkSource().getLightEngine(); + } + ++ // Folia start - region threading ++ @Nullable ++ public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) { ++ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ if (chunk != null) { ++ return chunk.getBlockState(pos); ++ } ++ return null; ++ } ++ ++ @Nullable ++ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { ++ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ if (chunk != null) { ++ return chunk.getBlockState(pos); ++ } ++ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); ++ return chunk.getBlockState(pos); ++ } ++ // Folia end - region threading ++ + @Override + public BlockState getBlockState(BlockPos pos) { + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -1437,18 +1477,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + + public void addBlockEntityTicker(TickingBlockEntity ticker) { +- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking + } + + protected void tickBlockEntities() { + ProfilerFiller gameprofilerfiller = Profiler.get(); + + gameprofilerfiller.push("blockEntities"); +- this.tickingBlockEntities = true; +- if (!this.pendingBlockEntityTickers.isEmpty()) { +- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); +- this.pendingBlockEntityTickers.clear(); +- } ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking ++ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking ++ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking ++ List blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking + + // Spigot start + // Iterator iterator = this.blockEntityTickers.iterator(); +@@ -1459,9 +1498,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + int tilesThisCycle = 0; + var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll + toRemove.add(null); // Paper - Fix MC-117075 +- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters +- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; +- TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition); ++ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking ++ TickingBlockEntity tickingblockentity = (TickingBlockEntity) blockEntityTickers.get(i); // Folia - regionised ticking + // Spigot end + + if (tickingblockentity.isRemoved()) { +@@ -1478,11 +1516,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + // Paper end - rewrite chunk system + } + } +- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 ++ blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 // Folia - regionised ticking + +- this.tickingBlockEntities = false; ++ regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking + gameprofilerfiller.pop(); +- this.spigotConfig.currentPrimedTnt = 0; // Spigot ++ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading + } + + public void guardEntityTick(Consumer tickConsumer, T entity) { +@@ -1494,7 +1532,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, throwable); + getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent +- entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Folia - properly disconnect players ++ if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players + // Paper end - Prevent block entity and entity crashes + } + this.moonrise$midTickTasks(); // Paper - rewrite chunk system +@@ -1555,9 +1594,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Nullable + public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return null; ++ } ++ // Folia end - region threading + // Paper start - Perf: Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; +- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) { ++ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(blockposition)) != null) { // Folia - region threading + return blockEntity; + } + // Paper end - Perf: Optimize capturedTileEntities lookup +@@ -1570,8 +1614,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + if (!this.isOutsideBuildHeight(blockposition)) { + // CraftBukkit start +- if (this.captureBlockStates) { +- this.capturedTileEntities.put(blockposition.immutable(), blockEntity); ++ if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading ++ this.getCurrentWorldData().capturedTileEntities.put(blockposition.immutable(), blockEntity); // Folia - region threading + return; + } + // CraftBukkit end +@@ -1651,6 +1695,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading + Profiler.get().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +@@ -1681,6 +1726,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public void getEntities(final EntityTypeTest entityTypeTest, + final AABB boundingBox, final Predicate predicate, + final List into, final int maxCount) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading + Profiler.get().incrementCounter("getEntities"); + + if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { +@@ -1780,13 +1826,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + public void disconnect() {} + ++ @Override // Folia - region threading + public long getGameTime() { +- return this.levelData.getGameTime(); ++ // Folia start - region threading ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); ++ // Folia end - region threading + } + + public long getDayTime() { +- return this.levelData.getDayTime(); ++ // Folia start - region threading ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); ++ // Folia end - region threading ++ } ++ ++ // Folia start - region threading ++ @Override ++ public long dayTime() { ++ return this.getDayTime(); ++ } ++ ++ @Override ++ public long getRedstoneGameTime() { ++ return this.getCurrentWorldData().getRedstoneGameTime(); + } ++ // Folia end - region threading + + public boolean mayInteract(Player player, BlockPos pos) { + return true; +@@ -1979,8 +2046,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public abstract RecipeAccess recipeAccess(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int l) { +- this.randValue = this.randValue * 3 + 1013904223; +- int i1 = this.randValue >> 2; ++ int i1 = this.random.nextInt() >> 2; // Folia - region threading + + return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); + } +@@ -2002,7 +2068,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public long nextSubTickCount() { +- return (long) (this.subTickCount++); ++ return this.subTickCount.getAndIncrement(); // Folia - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/LevelAccessor.java b/src/main/java/net/minecraft/world/level/LevelAccessor.java +index e5c5698fd74cb916467bccead8c1f323c6e3cbb0..9bfb933a2b2af849286d53bd05a77ee3c1f9e665 100644 +--- a/src/main/java/net/minecraft/world/level/LevelAccessor.java ++++ b/src/main/java/net/minecraft/world/level/LevelAccessor.java +@@ -34,14 +34,24 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess, Sch + + long nextSubTickCount(); + ++ // Folia start - region threading ++ default long getGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ ++ default long getRedstoneGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ // Folia end - region threading ++ + @Override + default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, priority, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, priority, this.nextSubTickCount()); // Folia - region threading + } + + @Override + default ScheduledTick createTick(BlockPos pos, T type, int delay) { +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, this.nextSubTickCount()); // Folia - region threading + } + + LevelData getLevelData(); +diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java +index ade435de0af4ee3566fa4a490df53cddd2f6531c..8fd969d3fe8c9ea8dcb53b4187897b401d1af4a1 100644 +--- a/src/main/java/net/minecraft/world/level/LevelReader.java ++++ b/src/main/java/net/minecraft/world/level/LevelReader.java +@@ -206,6 +206,25 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste + return maxY >= this.getMinY() && minY <= this.getMaxY() && this.hasChunksAt(minX, minZ, maxX, maxZ); + } + ++ // Folia start - region threading ++ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { ++ int i = SectionPos.blockToSectionCoord(minX); ++ int j = SectionPos.blockToSectionCoord(maxX); ++ int k = SectionPos.blockToSectionCoord(minZ); ++ int l = SectionPos.blockToSectionCoord(maxZ); ++ ++ for(int m = i; m <= j; ++m) { ++ for(int n = k; n <= l; ++n) { ++ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, m, n))) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ // Folia end - region threading ++ + @Deprecated + default boolean hasChunksAt(int minX, int minZ, int maxX, int maxZ) { + int i = SectionPos.blockToSectionCoord(minX); +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index c1b76a1ebc1eea7ab70cf61d8175a31794dd122a..5403794f9613ecd976769240ccecd3c174118b46 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -145,7 +145,7 @@ public final class NaturalSpawner { + int limit = enumcreaturetype.getMaxInstancesPerChunk(); + SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); + if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { +- spawnThisTick = worldserver.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % worldserver.ticksPerSpawnCategory.getLong(spawnCategory) == 0; ++ spawnThisTick = worldserver.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worldserver.getRedstoneGameTime() % worldserver.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading + limit = worldserver.getWorld().getSpawnLimit(spawnCategory); + } + +diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java +index 685ccfb73bf7125585ef90b6a0f51b2f81daa428..0b49da093cdae2c2738f7977f38b39f1e8a29876 100644 +--- a/src/main/java/net/minecraft/world/level/ServerExplosion.java ++++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java +@@ -795,17 +795,18 @@ public class ServerExplosion implements Explosion { + if (!this.level.paperConfig().environment.optimizeExplosions) { + return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations + } ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + CacheKey key = new CacheKey(this, entity.getBoundingBox()); +- Float blockDensity = this.level.explosionDensityCache.get(key); ++ Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading + if (blockDensity == null) { + blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations +- this.level.explosionDensityCache.put(key, blockDensity); ++ worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading + } + + return blockDensity; + } + +- static class CacheKey { ++ public static class CacheKey { // Folia - region threading - public + private final Level world; + private final double posX, posY, posZ; + private final double minX, minY, minZ; +diff --git a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +index 3d377b9e461040405e0a7dcbd72d1506b48eb44e..782890e227ff9dab44dd92327979c201985f116e 100644 +--- a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java ++++ b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +@@ -7,6 +7,12 @@ public interface ServerLevelAccessor extends LevelAccessor { + + ServerLevel getLevel(); + ++ // Folia start - region threading ++ default public StructureManager structureManager() { ++ throw new UnsupportedOperationException(); ++ } ++ // Folia end - region threading ++ + default void addFreshEntityWithPassengers(Entity entity) { + // CraftBukkit start + this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); +diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java +index 03adb02297911e5a5051edac14453d7e3eeac29c..cd2785d084225e8a166b21324d830cf20298c439 100644 +--- a/src/main/java/net/minecraft/world/level/StructureManager.java ++++ b/src/main/java/net/minecraft/world/level/StructureManager.java +@@ -48,11 +48,7 @@ public class StructureManager { + } + + public List startsForStructure(ChunkPos pos, Predicate predicate) { +- // Paper start - Fix swamp hut cat generation deadlock +- return this.startsForStructure(pos, predicate, null); +- } +- public List startsForStructure(ChunkPos pos, Predicate predicate, @Nullable ServerLevelAccessor levelAccessor) { +- Map map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); ++ Map map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading + // Paper end - Fix swamp hut cat generation deadlock + Builder builder = ImmutableList.builder(); + +@@ -121,20 +117,12 @@ public class StructureManager { + } + + public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate) { +- // Paper start - Fix swamp hut cat generation deadlock +- return this.getStructureWithPieceAt(pos, predicate, null); +- } +- +- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey tag, @Nullable ServerLevelAccessor levelAccessor) { +- return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor); +- } +- +- public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate, @Nullable ServerLevelAccessor levelAccessor) { ++ // Folia - region threading + // Paper end - Fix swamp hut cat generation deadlock + Registry registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE); + + for (StructureStart structureStart : this.startsForStructure( +- new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock ++ new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false) // Paper - Fix swamp hut cat generation deadlock // Folia - region threading + )) { + if (this.structureHasPieceAt(pos, structureStart)) { + return structureStart; +@@ -179,7 +167,7 @@ public class StructureManager { + } + + public void addReference(StructureStart structureStart) { +- structureStart.addReference(); ++ //structureStart.addReference(); // Folia - region threading - move to caller + this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure()); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java +index be700995c3e7f63e0471712c3cd6fd83db583491..421403e714949739d4bab2fba7b7896d73cbd99a 100644 +--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java +@@ -362,7 +362,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock + + world.setBlock(blockposition1, (BlockState) state.setValue(BedBlock.PART, BedPart.HEAD), 3); + // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states +- if (world.captureBlockStates) { ++ if (world.getCurrentWorldData().captureBlockStates) { // Folia - region threading + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index c0b1f903962b25d8ff6c2b4fcd2be0e45de09b35..9b952e74ee705256cbe9390553432f6364d4f2c1 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -382,8 +382,8 @@ public class Block extends BlockBehaviour implements ItemLike { + + entityitem.setDefaultPickUpDelay(); + // CraftBukkit start +- if (world.captureDrops != null) { +- world.captureDrops.add(entityitem); ++ if (world.getCurrentWorldData().captureDrops != null) { // Folia - region threading ++ world.getCurrentWorldData().captureDrops.add(entityitem); // Folia - region threading + } else { + world.addFreshEntity(entityitem); + } +diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java +index eb324fda54ada3ed7941713a784ed2d686ec8c4b..b66ff600fbad81679f1f5b6108abccabd39d84ec 100644 +--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java +@@ -31,7 +31,7 @@ public abstract class BushBlock extends Block { + // CraftBukkit start + if (!state.canSurvive(world, pos)) { + // Suppress during worldgen +- if (!(world instanceof net.minecraft.server.level.ServerLevel world1 && world1.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) { // Paper ++ if (!(world instanceof net.minecraft.server.level.ServerLevel world1 && world1.getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) { // Paper // Folia - region threading + return Blocks.AIR.defaultBlockState(); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +index 6e63ecbf425950f71bee9bf416cb6a77b6005ab6..7eca80ae9d5f8c3582d8c4de5cfae3705cadaecc 100644 +--- a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +@@ -114,7 +114,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { + } + + private static void tickEntity(Level world, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { +- if (world.getGameTime() % 20L == 0L) { ++ if (world.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + DaylightDetectorBlock.updateSignalStrength(state, world, pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +index 01584d77a8877528c3ec65971a1a6377c09e763b..0537418c9337d95acdc80eead47c9cbf397e7042 100644 +--- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +@@ -52,7 +52,7 @@ public class DispenserBlock extends BaseEntityBlock { + private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior(); + public static final Map DISPENSER_REGISTRY = new IdentityHashMap(); + private static final int TRIGGER_DURATION = 4; +- public static boolean eventFired = false; // CraftBukkit ++ public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading + + @Override + public MapCodec codec() { +@@ -109,7 +109,7 @@ public class DispenserBlock extends BaseEntityBlock { + + if (idispensebehavior != DispenseItemBehavior.NOOP) { + if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - Add BlockPreDispenseEvent +- DispenserBlock.eventFired = false; // CraftBukkit - reset event status ++ DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading + tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack)); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +index 4fe83bd0f355549847b66afb7e61f6f2a6d97016..fa140ec5a02cb9204a498864638e2da00709dedb 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +@@ -104,7 +104,7 @@ public class DoublePlantBlock extends BushBlock { + + protected static void preventDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) { + // CraftBukkit start +- if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper ++ if (((net.minecraft.server.level.ServerLevel)world).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java +index af46f2885ead1e3ec1734504d8ba134c886e04fb..d3848662cf2b2d4fc47c3bf321b9a25106cd572b 100644 +--- a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java +@@ -120,12 +120,38 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { + if (tileentity instanceof TheEndGatewayBlockEntity tileentityendgateway) { + Vec3 vec3d = tileentityendgateway.getPortalPosition(world, pos); + +- return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit ++ return vec3d == null ? null : getTeleportTransition(world, entity, vec3d); // Folia - region threading + } else { + return null; + } + } + ++ // Folia start - region threading ++ public static TeleportTransition getTeleportTransition(ServerLevel world, Entity entity, Vec3 targetPos) { ++ return (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, targetPos, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, targetPos, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit ++ } ++ ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ BlockEntity tile = sourceWorld.getBlockEntity(portalPos); ++ ++ if (!(tile instanceof TheEndGatewayBlockEntity endGateway)) { ++ return false; ++ } ++ ++ return TheEndGatewayBlockEntity.teleportRegionThreading( ++ sourceWorld, portalPos, portalTarget, endGateway, TeleportTransition.PLACE_PORTAL_TICKET ++ ); ++ } ++ // Folia end - region threading ++ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; +diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +index 8cb4142562db0be1f1a7d961ec5a10d4abf31692..45b8ac1418864ea32bd625b0d57fcec288ea5b0c 100644 +--- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +@@ -75,15 +75,7 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { + world.getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) return; // Paper - make cancellable + // CraftBukkit end +- if (!world.isClientSide && world.dimension() == Level.END && entity instanceof ServerPlayer) { +- ServerPlayer entityplayer = (ServerPlayer) entity; +- +- if (world.paperConfig().misc.disableEndCredits) entityplayer.seenCredits = true; // Paper - Option to disable end credits +- if (!entityplayer.seenCredits) { +- entityplayer.showEndCredits(); +- return; +- } +- } ++ // Folia - region threading - do not show credits + + entity.setAsInsidePortal(this, pos); + } +@@ -135,6 +127,20 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { + } + } + ++ // Folia start - region threading ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ return portalTarget.endPortalLogicAsync(portalPos); ++ } ++ // Folia end - region threading ++ + @Override + public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { + double d0 = (double) pos.getX() + random.nextDouble(); +diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +index c3dba0c2c94f3804338f86621dc42405e380a6b3..e979f0410a37da490f788242fa64b0bc26f75027 100644 +--- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java +@@ -93,8 +93,8 @@ public class FarmBlock extends Block { + @Override + protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { + int i = (Integer) state.getValue(FarmBlock.MOISTURE); +- if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks +- if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks ++ if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (world.getRedstoneGameTime() + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading ++ if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (world.getRedstoneGameTime() + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading + + if (!FarmBlock.isNearWater(world, pos) && !world.isRainingAt(pos.above())) { + if (i > 0) { +diff --git a/src/main/java/net/minecraft/world/level/block/FungusBlock.java b/src/main/java/net/minecraft/world/level/block/FungusBlock.java +index ba5fff8892593a921f57cd55ad60cd6744ab4cf1..844143011fbb3453557b6cbe562ae41c7646f454 100644 +--- a/src/main/java/net/minecraft/world/level/block/FungusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FungusBlock.java +@@ -76,9 +76,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { + this.getFeature(world).ifPresent((holder) -> { + // CraftBukkit start + if (this == Blocks.WARPED_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading + } else if (this == Blocks.CRIMSON_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading + } + // CraftBukkit end + ((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos); +diff --git a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +index 5c360c6768582c1a35431739613e9b406875cc21..7285dd70e73e1a15b6af6a679a1a0577afd39e44 100644 +--- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +@@ -94,7 +94,7 @@ public class HoneyBlock extends HalfTransparentBlock { + } + + private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { +- if (entity instanceof ServerPlayer && entity.level().getGameTime() % 20L == 0L) { ++ if (entity instanceof ServerPlayer && entity.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level().getBlockState(pos)); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +index a1fee00d12342cbd20e79096b405f4b108236d37..256c577e269d483ef64d1a091c36601b0a7d2293 100644 +--- a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +@@ -113,7 +113,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc + + @Override + public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { +- if (world.isThundering() && (long) world.random.nextInt(200) <= world.getGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { ++ if (world.isThundering() && (long) world.random.nextInt(200) <= world.getRedstoneGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { // Folia - region threading + ParticleUtils.spawnParticlesAlongAxis(((Direction) state.getValue(LightningRodBlock.FACING)).getAxis(), world, pos, 0.125D, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +index 7900856c9e86afe907c00c9ac31bec93ece50abe..c0843e7a0c173a734f5eab21172b5bbb0f3d3e3e 100644 +--- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +@@ -105,7 +105,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { + return false; + } else { + world.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit ++ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading + if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +index 3e9642e5236d9a1cc8e8f3b375d76810f4bc7c6c..88e33ae16848afe9885684537ef32e1836425fa3 100644 +--- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -178,6 +178,33 @@ public class NetherPortalBlock extends Block implements Portal { + } + } + ++ // Folia start - region threading ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ return portalTarget.netherPortalLogicAsync(portalPos); ++ } ++ ++ public static BlockUtil.FoundRectangle findPortalAround(ServerLevel world, BlockPos rough, WorldBorder worldBorder, int searchRadius) { ++ BlockPos found = world.getPortalForcer().findClosestPortalPosition(rough, worldBorder, searchRadius).orElse(null); ++ if (found == null) { ++ return null; ++ } ++ ++ BlockState portalState = world.getBlockStateFromEmptyChunk(found); ++ ++ return BlockUtil.getLargestRectangleAround(found, portalState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (pos) -> { ++ return world.getBlockStateFromEmptyChunk(pos) == portalState; ++ }); ++ } ++ // Folia end - region threading ++ + @Nullable + private TeleportTransition getExitPortal(ServerLevel worldserver, Entity entity, BlockPos blockposition, BlockPos blockposition1, boolean flag, WorldBorder worldborder, int searchRadius, boolean canCreatePortal, int createRadius) { + Optional optional = worldserver.getPortalForcer().findClosestPortalPosition(blockposition1, worldborder, searchRadius); +@@ -186,10 +213,10 @@ public class NetherPortalBlock extends Block implements Portal { + + if (optional.isPresent()) { + BlockPos blockposition2 = (BlockPos) optional.get(); +- BlockState iblockdata = worldserver.getBlockState(blockposition2); ++ BlockState iblockdata = worldserver.getBlockStateFromEmptyChunk(blockposition2); // Folia - region threading + + blockutil_rectangle = BlockUtil.getLargestRectangleAround(blockposition2, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition3) -> { +- return worldserver.getBlockState(blockposition3) == iblockdata; ++ return worldserver.getBlockStateFromEmptyChunk(blockposition3) == iblockdata; // Folia - region threading + }); + teleporttransition_a = TeleportTransition.PLAY_PORTAL_SOUND.then((entity1) -> { + entity1.placePortalTicket(blockposition2); +@@ -235,7 +262,7 @@ public class NetherPortalBlock extends Block implements Portal { + return NetherPortalBlock.createDimensionTransition(world, exitPortalRectangle, enumdirection_enumaxis, vec3d, entity, postDimensionTransition); + } + +- private static TeleportTransition createDimensionTransition(ServerLevel world, BlockUtil.FoundRectangle exitPortalRectangle, Direction.Axis axis, Vec3 positionInPortal, Entity entity, TeleportTransition.PostTeleportTransition postDimensionTransition) { ++ public static TeleportTransition createDimensionTransition(ServerLevel world, BlockUtil.FoundRectangle exitPortalRectangle, Direction.Axis axis, Vec3 positionInPortal, Entity entity, TeleportTransition.PostTeleportTransition postDimensionTransition) { // Folia - region threading - public + BlockPos blockposition = exitPortalRectangle.minCorner; + BlockState iblockdata = world.getBlockState(blockposition); + Direction.Axis enumdirection_enumaxis1 = (Direction.Axis) iblockdata.getOptionalValue(BlockStateProperties.HORIZONTAL_AXIS).orElse(Direction.Axis.X); +diff --git a/src/main/java/net/minecraft/world/level/block/Portal.java b/src/main/java/net/minecraft/world/level/block/Portal.java +index 35ed840c1aeb56432613ec346f16f793de1c498d..fce6ae0619e9c613208ce5eda4274fee384b561b 100644 +--- a/src/main/java/net/minecraft/world/level/block/Portal.java ++++ b/src/main/java/net/minecraft/world/level/block/Portal.java +@@ -14,6 +14,10 @@ public interface Portal { + @Nullable + TeleportTransition getPortalDestination(ServerLevel world, Entity entity, BlockPos pos); + ++ // Folia start - region threading ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos); ++ // Folia end - region threading ++ + default Portal.Transition getLocalTransition() { + return Portal.Transition.NONE; + } +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 21f2c61023fadcce30452a02f067cd5d87e5d8dc..0020e7bb7b19179a898cd8835d12cfa37eccdcc2 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -91,7 +91,7 @@ public class RedStoneWireBlock extends Block { + private static final float PARTICLE_DENSITY = 0.2F; + private final BlockState crossState; + private final RedstoneWireEvaluator evaluator = new DefaultRedstoneWireEvaluator(this); +- public boolean shouldSignal = true; ++ //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data + + @Override + public MapCodec codec() { +@@ -292,7 +292,11 @@ public class RedStoneWireBlock extends Block { + + // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java +- com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); ++ // Folia start - region threading ++ private com.destroystokyo.paper.util.RedstoneWireTurbo getTurbo(Level world) { ++ return world.getCurrentWorldData().turbo; ++ } ++ // Folia end - region threading + + /* + * Modified version of pre-existing updateSurroundingRedstone, which is called from +@@ -308,7 +312,7 @@ public class RedStoneWireBlock extends Block { + if (orientation != null) { + source = pos.relative(orientation.getFront().getOpposite()); + } +- turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading + return; + } + updatePowerStrength(worldIn, pos, state, orientation, blockAdded); +@@ -336,7 +340,7 @@ public class RedStoneWireBlock extends Block { + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { +- turbo.updateNeighborShapes(level, pos, state); ++ this.getTurbo(level).updateNeighborShapes(level, pos, state); // Folia - region threading + } + } + } +@@ -353,9 +357,9 @@ public class RedStoneWireBlock extends Block { + } + + public int getBlockSignal(Level world, BlockPos pos) { +- this.shouldSignal = false; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading + int i = world.getBestNeighborSignal(pos); +- this.shouldSignal = true; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading + return i; + } + +@@ -449,12 +453,12 @@ public class RedStoneWireBlock extends Block { + + @Override + protected int getDirectSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { +- return !this.shouldSignal ? 0 : state.getSignal(world, pos, direction); ++ return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : state.getSignal(world, pos, direction); // Folia - region threading + } + + @Override + protected int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { +- if (this.shouldSignal && direction != Direction.DOWN) { ++ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && direction != Direction.DOWN) { // Folia - region threading + int i = state.getValue(POWER); + if (i == 0) { + return 0; +@@ -486,7 +490,10 @@ public class RedStoneWireBlock extends Block { + + @Override + protected boolean isSignalSource(BlockState state) { +- return this.shouldSignal; ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ return worldData == null || worldData.shouldSignal; ++ // Folia end - region threading + } + + public static int getColorForPower(int powerLevel) { +diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +index 26319e8247e9dfa2068700d7fadc21ea5f715561..1d87310289ff28dacf3f5bfb8f3ae0593c533b25 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +@@ -82,10 +82,10 @@ public class RedstoneTorchBlock extends BaseTorchBlock { + protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { + boolean flag = this.hasNeighborSignal(world, pos, state); + // Paper start - Faster redstone torch rapid clock removal +- java.util.ArrayDeque redstoneUpdateInfos = world.redstoneUpdateInfos; ++ java.util.ArrayDeque redstoneUpdateInfos = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading + if (redstoneUpdateInfos != null) { + RedstoneTorchBlock.Toggle curr; +- while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) { ++ while ((curr = redstoneUpdateInfos.peek()) != null && world.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading + redstoneUpdateInfos.poll(); + } + } +@@ -166,14 +166,14 @@ public class RedstoneTorchBlock extends BaseTorchBlock { + + private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) { + // Paper start - Faster redstone torch rapid clock removal +- java.util.ArrayDeque list = world.redstoneUpdateInfos; ++ java.util.ArrayDeque list = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading + if (list == null) { +- list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>(); ++ list = world.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading + } + // Paper end - Faster redstone torch rapid clock removal + + if (addNew) { +- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime())); ++ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getRedstoneGameTime())); // Folia - region threading + } + + int i = 0; +@@ -200,12 +200,18 @@ public class RedstoneTorchBlock extends BaseTorchBlock { + + public static class Toggle { + +- final BlockPos pos; +- final long when; ++ public final BlockPos pos; // Folia - region threading ++ long when; // Folia - region ticking + + public Toggle(BlockPos pos, long time) { + this.pos = pos; + this.when = time; + } ++ ++ // Folia start - region ticking ++ public void offsetTime(long offset) { ++ this.when += offset; ++ } ++ // Folia end - region ticking + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +index d262a5a6da57ef9ba9a6fe0dfbc88f577105e74f..e842c05cfe5991ba33582b9399610affbd02913f 100644 +--- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +@@ -35,7 +35,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { + protected static final float AABB_OFFSET = 6.0F; + protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D); + protected final TreeGrower treeGrower; +- public static TreeType treeType; // CraftBukkit ++ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading + + @Override + public MapCodec codec() { +@@ -66,18 +66,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { + world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4); + } else { + // CraftBukkit start +- if (world.captureTreeGeneration) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ if (worldData.captureTreeGeneration) { // Folia - region threading + this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + } else { +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Folia - region threading + this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); +- world.captureTreeGeneration = false; +- if (world.capturedBlockStates.size() > 0) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ worldData.captureTreeGeneration = false; // Folia - region threading ++ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ SaplingBlock.treeTypeRT.set(null); // Folia - region threading + Location location = CraftLocation.toBukkit(pos, world.getWorld()); +- java.util.List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + StructureGrowEvent event = null; + if (treeType != null) { + event = new StructureGrowEvent(location, treeType, false, null, blocks); +diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +index 4e38d2e0ce156b4edbef67c82c845ceeebbbcd59..5e761ca2a8f1edaba2305c99f1310f3d7cab9cac 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +@@ -54,7 +54,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + + @Override + protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { +- if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks ++ if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - regionised ticking + // Paper start - Perf: optimize dirt and snow spreading + final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); + if (cachedBlockChunk == null) { // Is this needed? +diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +index 0fbe66cc02bd3d95c0a5dcd55380a1b4a2f17ca6..3e3fc48470cc058d84f8db3fa8dc6105eb52eb89 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -62,7 +62,7 @@ public class WitherSkullBlock extends SkullBlock { + } + + public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) { +- if (world.captureBlockStates) return; // CraftBukkit ++ if (world.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading + if (!world.isClientSide) { + BlockState iblockdata = blockEntity.getBlockState(); + boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +index 0e0d178f2793ab014358f534c8dc53218b89f083..7144c4dd1091edc3bbaa0f862763e3e60f520c9e 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -215,7 +215,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + + i1 = blockEntity.levels; +- if (world.getGameTime() % 80L == 0L) { ++ if (world.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading + if (!blockEntity.beamSections.isEmpty()) { + blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); + } +@@ -339,7 +339,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + list = world.getEntitiesOfClass(Player.class, axisalignedbb); + } else { + list = new java.util.ArrayList<>(); +- for (Player player : world.players()) { ++ for (Player player : world.getLocalPlayers()) { // Folia - region threading + if (player.isSpectator()) { + continue; + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +index 1f664c10138a6e19bdc0051fa80575516d5602e7..edb7b9391855eba5d693f3d64b6fb14a1b1c4949 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -33,7 +33,7 @@ import org.bukkit.inventory.InventoryHolder; + // CraftBukkit end + + public abstract class BlockEntity { +- static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers ++ static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading + + // CraftBukkit start - data containers + private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry(); +@@ -48,6 +48,12 @@ public abstract class BlockEntity { + private BlockState blockState; + private DataComponentMap components; + ++ // Folia start - region ticking ++ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { ++ ++ } ++ // Folia end - region ticking ++ + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + this.components = DataComponentMap.EMPTY; + this.type = type; +@@ -216,7 +222,7 @@ public abstract class BlockEntity { + + public void setChanged() { + if (this.level != null) { +- if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers ++ if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading + BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java +index e2123f1c5852a34fb92e900f25591284625f3494..a6009ce84f0f7795f132b5b7f9517ff1bd2da971 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java +@@ -61,6 +61,13 @@ public class CommandBlockEntity extends BlockEntity { + return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), this.getLevel().paperConfig().commandBlocks.permissionsLevel, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null); // Paper - configurable command block perm level + } + ++ // Folia start ++ @Override ++ public void threadCheck() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block"); ++ } ++ // Folia end ++ + @Override + public boolean isValid() { + return !CommandBlockEntity.this.isRemoved(); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index d354c2ad41533ec8d52f7c5f63f8f74a0b1ce235..290db66d88061edba14fefdd261364a82624a574 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -85,7 +85,7 @@ public class ConduitBlockEntity extends BlockEntity { + + public static void clientTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + ++blockEntity.tickCount; +- long i = world.getGameTime(); ++ long i = world.getRedstoneGameTime(); // Folia - region threading + List list = blockEntity.effectBlocks; + + if (i % 40L == 0L) { +@@ -103,7 +103,7 @@ public class ConduitBlockEntity extends BlockEntity { + + public static void serverTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + ++blockEntity.tickCount; +- long i = world.getGameTime(); ++ long i = world.getRedstoneGameTime(); // Folia - region threading + List list = blockEntity.effectBlocks; + + if (i % 40L == 0L) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 5ebbdb94d9b91c442ff60eb6872f740ebd790fa0..44aae845da6cd34fc00e0c71795d6f610679bd4b 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -49,7 +49,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + private static final int[][] CACHED_SLOTS = new int[54][]; + private NonNullList items; + public int cooldownTime; +- private long tickedGameTime; ++ private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading + private Direction facing; + + // CraftBukkit start - add fields and methods +@@ -82,6 +82,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + // CraftBukkit end + ++ // Folia start - region threading ++ @Override ++ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.tickedGameTime != Long.MIN_VALUE) { ++ this.tickedGameTime += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region threading ++ + public HopperBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.HOPPER, pos, state); + this.items = NonNullList.withSize(5, ItemStack.EMPTY); +@@ -141,7 +151,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + public static void pushItemsTick(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) { + --blockEntity.cooldownTime; +- blockEntity.tickedGameTime = world.getGameTime(); ++ blockEntity.tickedGameTime = world.getRedstoneGameTime(); // Folia - region threading + if (!blockEntity.isOnCooldown()) { + blockEntity.setCooldown(0); + // Spigot start +@@ -237,12 +247,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + // Paper start - Perf: Optimize Hoppers +- private static boolean skipPullModeEventFire; +- private static boolean skipPushModeEventFire; +- public static boolean skipHopperEvents; ++ // Folia - region threading - moved to RegionizedWorldData + + private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { +- skipPushModeEventFire = skipHopperEvents; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading + boolean foundItem = false; + for (int i = 0; i < hopper.getContainerSize(); ++i) { + final ItemStack item = hopper.getItem(i); +@@ -257,7 +266,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + // We only need to fire the event once to give protection plugins a chance to cancel this event + // Because nothing uses getItem, every event call should end up the same result. +- if (!skipPushModeEventFire) { ++ if (!worldData.skipPushModeEventFire) { // Folia - region threading + movedItem = callPushMoveEvent(destination, movedItem, hopper); + if (movedItem == null) { // cancelled + origItemStack.setCount(originalItemCount); +@@ -287,13 +296,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + ItemStack movedItem = origItemStack; + final int originalItemCount = origItemStack.getCount(); + final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); + container.setChanged(); // original logic always marks source inv as changed even if no move happens. + movedItem.setCount(movedItemCount); + +- if (!skipPullModeEventFire) { ++ if (!worldData.skipPullModeEventFire) { // Folia - region threading + movedItem = callPullMoveEvent(hopper, container, movedItem); + if (movedItem == null) { // cancelled + origItemStack.setCount(originalItemCount); +@@ -313,9 +323,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); + } + +- ignoreTileUpdates = true; ++ IGNORE_TILE_UPDATES.set(true); // Folia - region threading + container.setItem(i, origItemStack); +- ignoreTileUpdates = false; ++ IGNORE_TILE_UPDATES.set(false); // Folia - region threading + container.setChanged(); + return true; + } +@@ -330,12 +340,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + final Inventory destinationInventory = getInventory(iinventory); + final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), + CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); + final boolean result = event.callEvent(); + if (!event.calledGetItem && !event.calledSetItem) { +- skipPushModeEventFire = true; ++ worldData.skipPushModeEventFire = true; // Folia - region threading + } + if (!result) { + cooldownHopper(hopper); +@@ -351,6 +362,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + final Inventory sourceInventory = getInventory(container); + final Inventory destination = getInventory(hopper); + +@@ -358,7 +370,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); + final boolean result = event.callEvent(); + if (!event.calledGetItem && !event.calledSetItem) { +- skipPullModeEventFire = true; ++ worldData.skipPullModeEventFire = true; // Folia - region threading + } + if (!result) { + cooldownHopper(hopper); +@@ -546,13 +558,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + public static boolean suckInItems(Level world, Hopper hopper) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + BlockPos blockposition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ()); + BlockState iblockdata = world.getBlockState(blockposition); + Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper, blockposition, iblockdata); + + if (iinventory != null) { + Direction enumdirection = Direction.DOWN; +- skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers ++ worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading + int[] aint = HopperBlockEntity.getSlots(iinventory, enumdirection); + int i = aint.length; + +@@ -743,9 +756,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + stack = stack.split(to.getMaxStackSize()); + } + // Spigot end +- ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers ++ IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading + to.setItem(slot, stack); +- ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers ++ IGNORE_TILE_UPDATES.set(Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading + stack = leftover; // Paper - Make hoppers respect inventory max stack size + flag = true; + } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 4729befa12732a9fd65cce243b33b3b479026c41..8ad84c8fce0461457bf918b22cf55a0c47a2dbcc 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -46,9 +46,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + // Paper end - Fix NPE in SculkBloomEvent world access + + public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading + blockEntity.catalystListener.getSculkSpreader().updateCursors(world, pos, world.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index 68fd5d3f6553af8af867e34946cb8b3f852da985..cccd9a2f4d13adaf5fe677fef6f8f9db72d9d87f 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -39,9 +39,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + public long age; + private int teleportCooldown; + @Nullable +- public BlockPos exitPortal; ++ public volatile BlockPos exitPortal; // Folia - region threading - volatile + public boolean exactTeleport; + ++ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading ++ private Long searchingForExitId; // Folia - region threading ++ + public TheEndGatewayBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.END_GATEWAY, pos, state); + } +@@ -140,6 +143,104 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + } + } + ++ // Folia start - region threading ++ private void trySearchForExit(ServerLevel world, BlockPos fromPos) { ++ if (this.searchingForExitId != null) { ++ return; ++ } ++ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); ++ int chunkX = fromPos.getX() >> 4; ++ int chunkZ = fromPos.getZ() >> 4; ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ complete.addWaiter((tpLoc, throwable) -> { ++ // create the exit portal ++ TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); ++ TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); ++ ++ // need to go onto the tick thread to avoid saving issues ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // update the exit portal location ++ TheEndGatewayBlockEntity.this.exitPortal = tpLoc; ++ ++ // remove ticket keeping the gateway loaded ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ TheEndGatewayBlockEntity.this.searchingForExitId = null; ++ } ++ ); ++ }); ++ ++ findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); ++ } ++ ++ public static boolean teleportRegionThreading(ServerLevel portalWorld, BlockPos portalPos, ++ net.minecraft.world.entity.Entity toTeleport, ++ TheEndGatewayBlockEntity portalTile, ++ net.minecraft.world.level.portal.TeleportTransition.PostTeleportTransition post) { ++ // can we even teleport in this dimension? ++ if (portalTile.exitPortal == null && portalWorld.getTypeKey() != LevelStem.END) { ++ return false; ++ } ++ ++ // First, find the position we are trying to teleport to ++ BlockPos teleportPos = portalTile.exitPortal; ++ boolean isExactTeleport = portalTile.exactTeleport; ++ ++ if (teleportPos == null) { ++ portalTile.trySearchForExit(portalWorld, portalPos); ++ return false; ++ } ++ ++ // note: we handle the position from the TeleportTransition ++ net.minecraft.world.level.portal.TeleportTransition teleport = net.minecraft.world.level.block.EndGatewayBlock.getTeleportTransition( ++ portalWorld, toTeleport, Vec3.atCenterOf(teleportPos) ++ ); ++ ++ ++ if (isExactTeleport) { ++ // blind teleport ++ return toTeleport.teleportAsync( ++ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ post == null ? null : (net.minecraft.world.entity.Entity teleportedEntity) -> { ++ post.onTransition(teleportedEntity); ++ } ++ ); ++ } else { ++ // we could hack around by first loading the chunks, then calling back to here and checking if the entity ++ // should be teleported, something something else... ++ // however, we know the target location cannot differ by one region section: so we can ++ // just teleport and adjust the position after ++ return toTeleport.teleportAsync( ++ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ (net.minecraft.world.entity.Entity teleportedEntity) -> { ++ // adjust to the final exit position ++ Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(portalWorld, teleportPos)); ++ // teleportTo will adjust rider positions ++ teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); ++ ++ if (post != null) { ++ post.onTransition(teleportedEntity); ++ } ++ } ++ ); ++ } ++ } ++ // Folia end - region threading ++ + @Nullable + public Vec3 getPortalPosition(ServerLevel world, BlockPos pos) { + BlockPos blockposition1; +@@ -189,6 +290,124 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + return TheEndGatewayBlockEntity.findTallestBlock(world, blockposition1, 16, true); + } + ++ // Folia start - region threading ++ private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ tentativeSelection.addWaiter((vec3d, throwable) -> { ++ LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); ++ BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); ++ if (blockposition1 == null) { ++ BlockPos blockposition2 = BlockPos.containing(vec3d.x + 0.5D, 75.0D, vec3d.z + 0.5D); ++ ++ TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); ++ world.registryAccess().lookup(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { ++ return iregistry.get(EndFeatures.END_ISLAND); ++ }).ifPresent((holder_c) -> { ++ ((ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); ++ }); ++ blockposition1 = blockposition2; ++ } else { ++ TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); ++ } ++ ++ // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained ++ // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue ++ // (regioniser only guarantees at least 8 chunks along a single axis) ++ // so, we need to schedule for the next tick ++ int posX = blockposition1.getX(); ++ int posZ = blockposition1.getZ(); ++ int radius = 16; ++ ++ BlockPos finalBlockPosition1 = blockposition1; ++ world.moonrise$loadChunksAsync(blockposition1, radius, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (java.util.List chunks) -> { ++ // make sure chunks are kept loaded ++ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { ++ world.chunkSource.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.DELAYED, access.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ } ++ // now after the chunks are loaded, we can delay by one tick ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, posX >> 4, posZ >> 4, () -> { ++ // find final location ++ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true).above(GATEWAY_HEIGHT_ABOVE_SURFACE); ++ ++ // done ++ complete.complete(tpLoc); ++ } ++ ); ++ } ++ ); ++ }); ++ ++ // fire off chain ++ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); ++ } ++ ++ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { ++ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); ++ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); ++ ++ class Vars { ++ int i = 16; ++ boolean mode = false; ++ Vec3 currPos = posDirExtruded; ++ } ++ Vars vars = new Vars(); ++ ++ Runnable handle = new Runnable() { ++ @Override ++ public void run() { ++ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { ++ vars.i = 0; // fall back to completing ++ } ++ ++ // try to load next chunk ++ if (vars.i-- <= 0) { ++ if (vars.mode) { ++ complete.complete(vars.currPos); ++ return; ++ } ++ vars.mode = true; ++ vars.i = 16; ++ } ++ ++ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); ++ // schedule next iteration ++ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(vars.currPos), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(vars.currPos), ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunk) -> { ++ this.run(); ++ } ++ ); ++ } ++ }; ++ ++ // kick off first chunk load ++ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(posDirExtruded), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(posDirExtruded), ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunk) -> { ++ handle.run(); ++ } ++ ); ++ } ++ // Folia end - region threading ++ + private static Vec3 findExitPortalXZPosTentative(ServerLevel world, BlockPos pos) { + Vec3 vec3d = (new Vec3((double) pos.getX(), 0.0D, (double) pos.getZ())).normalize(); + boolean flag = true; +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java +index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java +@@ -10,4 +10,6 @@ public interface TickingBlockEntity { + BlockPos getPos(); + + String getType(); ++ ++ BlockEntity getTileEntity(); // Folia - region threading + } +diff --git a/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java b/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java +index 0a39c3ceaf091eeb9f8af979d0851bbda7bb2586..e1a67d8d4b2ad48b4f6c7baf19c028fb011c63eb 100644 +--- a/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java ++++ b/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java +@@ -175,55 +175,57 @@ public final class TreeGrower { + // CraftBukkit start + private void setTreeType(Holder> holder) { + ResourceKey> worldgentreeabstract = holder.unwrapKey().get(); ++ TreeType treeType; // Folia - region threading + if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) { +- SaplingBlock.treeType = TreeType.TREE; ++ treeType = TreeType.TREE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) { +- SaplingBlock.treeType = TreeType.RED_MUSHROOM; ++ treeType = TreeType.RED_MUSHROOM; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) { +- SaplingBlock.treeType = TreeType.BROWN_MUSHROOM; ++ treeType = TreeType.BROWN_MUSHROOM; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) { +- SaplingBlock.treeType = TreeType.COCOA_TREE; ++ treeType = TreeType.COCOA_TREE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) { +- SaplingBlock.treeType = TreeType.SMALL_JUNGLE; ++ treeType = TreeType.SMALL_JUNGLE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.PINE) { +- SaplingBlock.treeType = TreeType.TALL_REDWOOD; ++ treeType = TreeType.TALL_REDWOOD; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.SPRUCE) { +- SaplingBlock.treeType = TreeType.REDWOOD; ++ treeType = TreeType.REDWOOD; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.ACACIA) { +- SaplingBlock.treeType = TreeType.ACACIA; ++ treeType = TreeType.ACACIA; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) { +- SaplingBlock.treeType = TreeType.BIRCH; ++ treeType = TreeType.BIRCH; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) { +- SaplingBlock.treeType = TreeType.TALL_BIRCH; ++ treeType = TreeType.TALL_BIRCH; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) { +- SaplingBlock.treeType = TreeType.SWAMP; ++ treeType = TreeType.SWAMP; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) { +- SaplingBlock.treeType = TreeType.BIG_TREE; ++ treeType = TreeType.BIG_TREE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) { +- SaplingBlock.treeType = TreeType.JUNGLE_BUSH; ++ treeType = TreeType.JUNGLE_BUSH; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.DARK_OAK) { +- SaplingBlock.treeType = TreeType.DARK_OAK; ++ treeType = TreeType.DARK_OAK; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) { +- SaplingBlock.treeType = TreeType.MEGA_REDWOOD; ++ treeType = TreeType.MEGA_REDWOOD; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) { +- SaplingBlock.treeType = TreeType.MEGA_PINE; ++ treeType = TreeType.MEGA_PINE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) { +- SaplingBlock.treeType = TreeType.JUNGLE; ++ treeType = TreeType.JUNGLE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) { +- SaplingBlock.treeType = TreeType.AZALEA; ++ treeType = TreeType.AZALEA; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.MANGROVE) { +- SaplingBlock.treeType = TreeType.MANGROVE; ++ treeType = TreeType.MANGROVE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) { +- SaplingBlock.treeType = TreeType.TALL_MANGROVE; ++ treeType = TreeType.TALL_MANGROVE; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) { +- SaplingBlock.treeType = TreeType.CHERRY; ++ treeType = TreeType.CHERRY; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.PALE_OAK || worldgentreeabstract == TreeFeatures.PALE_OAK_BONEMEAL) { +- SaplingBlock.treeType = TreeType.PALE_OAK; ++ treeType = TreeType.PALE_OAK; // Folia - region threading + } else if (worldgentreeabstract == TreeFeatures.PALE_OAK_CREAKING) { +- SaplingBlock.treeType = TreeType.PALE_OAK_CREAKING; ++ treeType = TreeType.PALE_OAK_CREAKING; // Folia - region threading + } else { + throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract); + } ++ SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading + } + // CraftBukkit end + +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java +index 4b51472502d08ea357da437afeb4b581979e9cff..0a7b67863e6f75bfcde877d5d226ab60dbe5a81b 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -156,7 +156,7 @@ public class PistonBaseBlock extends DirectionalBlock { + if (tileentity instanceof PistonMovingBlockEntity) { + PistonMovingBlockEntity tileentitypiston = (PistonMovingBlockEntity) tileentity; + +- if (tileentitypiston.isExtending() && (tileentitypiston.getProgress(0.0F) < 0.5F || world.getGameTime() == tileentitypiston.getLastTicked() || ((ServerLevel) world).isHandlingTick())) { ++ if (tileentitypiston.isExtending() && (tileentitypiston.getProgress(0.0F) < 0.5F || world.getRedstoneGameTime() == tileentitypiston.getLastTicked() || ((ServerLevel) world).isHandlingTick())) { // Folia - region threading + b0 = 2; + } + } +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 e1c9a961064887070b29207efd7af47884f8dc29..c3a04ef842630b3df447dea48b84bccde0c89e83 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 +@@ -41,9 +41,19 @@ public class PistonMovingBlockEntity extends BlockEntity { + private static final ThreadLocal NOCLIP = ThreadLocal.withInitial(() -> null); + private float progress; + private float progressO; +- private long lastTicked; ++ private long lastTicked = Long.MIN_VALUE; // Folia - region threading + private int deathTicks; + ++ // Folia start - region threading ++ @Override ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.lastTicked != Long.MIN_VALUE) { ++ this.lastTicked += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region threading ++ + public PistonMovingBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.PISTON, pos, state); + } +@@ -150,8 +160,8 @@ 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); ++ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading ++ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading + // Paper end + break; + } +@@ -299,7 +309,7 @@ public class PistonMovingBlockEntity extends BlockEntity { + } + + public static void tick(Level world, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) { +- blockEntity.lastTicked = world.getGameTime(); ++ blockEntity.lastTicked = world.getRedstoneGameTime(); // Folia - region threading + blockEntity.progressO = blockEntity.progress; + if (blockEntity.progressO >= 1.0F) { + if (world.isClientSide && blockEntity.deathTicks < 5) { +diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java +index 807a097a7b6399f24ede741f94ce98eb67e55add..c2a0cd1c25b6d775b55f8c3aacca9837e35a5dfd 100644 +--- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java ++++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java +@@ -34,6 +34,8 @@ public class WorldBorder { + + public WorldBorder() {} + ++ // Folia - region threading - TODO make this shit thread-safe ++ + public boolean isWithinBounds(BlockPos pos) { + return this.isWithinBounds((double) pos.getX(), (double) pos.getZ()); + } +@@ -47,14 +49,12 @@ public class WorldBorder { + } + + // Paper start - Bound treasure maps to world border +- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); ++ private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading + public boolean isBlockInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(chunkX, 64, chunkZ); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading + } + public boolean isChunkInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading + } + // Paper end - Bound treasure maps to world border + +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index e0cb360ece042c4fc6aa0d10106923fe25288f5c..7f6dd454e0794739dc1861f768aaed86c484afe7 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -326,7 +326,7 @@ public abstract class ChunkGenerator { + } + + private static boolean tryAddReference(StructureManager structureAccessor, StructureStart start) { +- if (start.canBeReferenced()) { ++ if (start.tryReference()) { // Folia - region threading + structureAccessor.addReference(start); + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 97937e3bd211997f0a0a3e9e671a1c59712d0003..2c421ddb759eae4fbf2f2420d3b8cab22e854b85 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -62,6 +62,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + @Override + public void tick() {} + ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return null; ++ } ++ // Folia end - region threading ++ + @Override + public boolean isRemoved() { + return true; +@@ -223,12 +230,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + @Override + public void markUnsaved() { +- boolean flag = this.isUnsaved(); +- +- super.markUnsaved(); +- if (!flag) { +- this.unsavedListener.setUnsaved(this.chunkPos); +- } ++ super.markUnsaved(); // Folia - region threading - unsavedListener is not really used + + } + +@@ -380,6 +382,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + @Nullable + public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, blockposition, "Updating block asynchronously"); // Folia - region threading + // CraftBukkit end + int i = blockposition.getY(); + LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i)); +@@ -421,7 +424,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + boolean flag3 = iblockdata1.hasBlockEntity(); + +- if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent ++ if (!this.level.isClientSide && !this.level.getCurrentWorldData().isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading + iblockdata1.onRemove(this.level, blockposition, iblockdata, flag); + } else if (!iblockdata1.is(block) && flag3) { + this.removeBlockEntity(blockposition); +@@ -431,7 +434,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + return null; + } else { + // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. +- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { ++ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Folia - region threading + iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); + } + +@@ -483,7 +486,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + @Nullable + public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { + // CraftBukkit start +- BlockEntity tileentity = this.level.capturedTileEntities.get(pos); ++ BlockEntity tileentity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading + if (tileentity == null) { + tileentity = (BlockEntity) this.blockEntities.get(pos); + } +@@ -705,13 +708,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + org.bukkit.World world = this.level.getWorld(); + if (world != null) { +- this.level.populating = true; ++ this.level.getCurrentWorldData().populating = true; // Folia - region threading + try { + for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { + populator.populate(world, random, bukkitChunk); + } + } finally { +- this.level.populating = false; ++ this.level.getCurrentWorldData().populating = false; // Folia - region threading + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); +@@ -737,7 +740,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + @Override + public boolean isUnsaved() { + // Paper start - rewrite chunk system +- final long gameTime = this.level.getGameTime(); ++ final long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading + if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) + || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { + return true; +@@ -1020,6 +1023,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + this.ticker = wrapped; + } + ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return this.ticker == null ? null : this.ticker.getTileEntity(); ++ } ++ // Folia end - region threading ++ + @Override + public void tick() { + this.ticker.tick(); +@@ -1056,6 +1066,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + this.ticker = blockentityticker; + } + ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return this.blockEntity; ++ } ++ // Folia end - region threading ++ + @Override + public void tick() { + if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +index 018b24d7611c3fd11536441431abf8f125850129..ceea49a2b04d0493cfa8ad6629116e0c1a9c97b3 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -562,7 +562,7 @@ public record SerializableChunkData(Registry biomeRegistry, ChunkPos chun + } + } + +- ChunkAccess.PackedTicks ichunkaccess_a = chunk.getTicksForSerialization(world.getGameTime()); ++ ChunkAccess.PackedTicks ichunkaccess_a = chunk.getTicksForSerialization(world.getRedstoneGameTime()); // Folia - region threading + ShortList[] ashortlist = (ShortList[]) Arrays.stream(chunk.getPostProcessing()).map((shortlist) -> { + return shortlist != null ? new ShortArrayList(shortlist) : null; + }).toArray((k) -> { +diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +index b331c93c82c27f9456fec208a0c008c5bedfa8c4..e5513271a24711f46e9107cbe526d533afa54ca5 100644 +--- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java ++++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +@@ -77,7 +77,7 @@ public class EndDragonFight { + private static final Component DEFAULT_BOSS_EVENT_NAME = Component.translatable("entity.minecraft.ender_dragon"); // Paper - ensure reset EnderDragon boss event name + public final ServerBossEvent dragonEvent; + public final ServerLevel level; +- private final BlockPos origin; ++ public final BlockPos origin; // Folia - region threading + public final ObjectArrayList gateways; + private final BlockPattern exitPortalPattern; + private int ticksSinceDragonSeen; +@@ -155,7 +155,7 @@ public class EndDragonFight { + + if (!this.dragonEvent.getPlayers().isEmpty()) { + this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); +- boolean flag = this.isArenaLoaded(); ++ boolean flag = this.isArenaLoaded(); if (!flag) { return; } // Folia - region threading - don't tick if we don't own the entire region + + if (this.needsStateScanning && flag) { + this.scanState(); +@@ -204,6 +204,12 @@ public class EndDragonFight { + } + + List list = this.level.getDragons(); ++ // Folia start - region threading ++ // we do not want to deal with any dragons NOT nearby ++ list.removeIf((dragon) -> { ++ return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon); ++ }); ++ // Folia end - region threading + + if (list.isEmpty()) { + this.dragonKilled = true; +@@ -350,9 +356,8 @@ public class EndDragonFight { + + for (int i = -8 + chunkcoordintpair.x; i <= 8 + chunkcoordintpair.x; ++i) { + for (int j = 8 + chunkcoordintpair.z; j <= 8 + chunkcoordintpair.z; ++j) { +- ChunkAccess ichunkaccess = this.level.getChunk(i, j, ChunkStatus.FULL, false); +- +- if (!(ichunkaccess instanceof LevelChunk)) { ++ ChunkAccess ichunkaccess = this.level.getChunkIfLoaded(i, j); // Folia - region threading ++ if (!(ichunkaccess instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Folia - region threading + return false; + } + +@@ -539,6 +544,11 @@ public class EndDragonFight { + } + + public void onCrystalDestroyed(EndCrystal enderCrystal, DamageSource source) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { ++ return; ++ } ++ // Folia end - region threading + if (this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) { + EndDragonFight.LOGGER.debug("Aborting respawn sequence"); + this.respawnStage = null; +@@ -569,7 +579,7 @@ public class EndDragonFight { + + public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal + // Paper end - Perf: Do crystal-portal proximity check before entity lookup +- if (this.dragonKilled && this.respawnStage == null) { ++ if (this.dragonKilled && this.respawnStage == null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading + BlockPos blockposition = this.portalLocation; + + if (blockposition == null) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +index e93ef232b0426a1095dad05fc4acb2a74db5b689..6e0fafa0c8f8e91d10ec0cb71742d137e5522155 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +@@ -18,7 +18,7 @@ import net.minecraft.world.level.block.state.BlockState; + + public class PatrolSpawner implements CustomSpawner { + +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + public PatrolSpawner() {} + +@@ -31,15 +31,16 @@ public class PatrolSpawner implements CustomSpawner { + return 0; + } else { + RandomSource randomsource = world.random; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + + // Paper start - Pillager patrol spawn settings and per player options + // Random player selection moved up for per player spawning and configuration +- int j = world.players().size(); ++ int j = world.getLocalPlayers().size(); // Folia - region threading + if (j < 1) { + return 0; + } + +- net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j)); ++ net.minecraft.server.level.ServerPlayer entityhuman = world.getLocalPlayers().get(randomsource.nextInt(j)); // Folia - region threading + if (entityhuman.isSpectator()) { + return 0; + } +@@ -49,8 +50,8 @@ public class PatrolSpawner implements CustomSpawner { + --entityhuman.patrolSpawnDelay; + patrolSpawnDelay = entityhuman.patrolSpawnDelay; + } else { +- this.nextTick--; +- patrolSpawnDelay = this.nextTick; ++ worldData.patrolSpawnerNextTick--; // Folia - region threading ++ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading + } + + if (patrolSpawnDelay > 0) { +@@ -65,7 +66,7 @@ public class PatrolSpawner implements CustomSpawner { + if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { + entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); + } else { +- this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); ++ worldData.patrolSpawnerNextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); // Folia - region threading + } + + if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 021221da5d0315f6e371380a705ac6b3f6ac18d3..ee9a0e0ce075e932a687d77594c687384ac19fb1 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -21,7 +21,7 @@ import net.minecraft.world.level.material.FluidState; + + public class PhantomSpawner implements CustomSpawner { + +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + public PhantomSpawner() {} + +@@ -39,20 +39,22 @@ public class PhantomSpawner implements CustomSpawner { + // Paper end - Ability to control player's insomnia and phantoms + RandomSource randomsource = world.random; + +- --this.nextTick; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ ++ --worldData.phantomSpawnerNextTick; // Folia - region threading ++ if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading + return 0; + } else { + // Paper start - Ability to control player's insomnia and phantoms + int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; + int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; +- this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; ++ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading + // Paper end - Ability to control player's insomnia and phantoms + if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { + return 0; + } else { + int i = 0; +- Iterator iterator = world.players().iterator(); ++ Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +index ff1f151b342a1567605f92a921fc7ab01f1c4807..27f428fc52cd371983c4f06329d22961ed32a15c 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +@@ -55,7 +55,7 @@ public class EndPlatformFeature extends Feature { + } + // CraftBukkit start + // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event +- if (entity != null) { ++ if (false) { // Folia - region threading + org.bukkit.World bworld = worldaccess.getLevel().getWorld(); + PortalCreateEvent portalEvent = new PortalCreateEvent((List) (List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); + +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java +index d871d760581df082c47365ef246681faafb1f6a0..08e0fec629a74551a0a25990fea7ee9380a1021d 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java +@@ -28,7 +28,7 @@ public final class StructureStart { + private final Structure structure; + private final PiecesContainer pieceContainer; + private final ChunkPos chunkPos; +- private int references; ++ private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading + @Nullable + private volatile BoundingBox cachedBoundingBox; + +@@ -41,7 +41,7 @@ public final class StructureStart { + public StructureStart(Structure structure, ChunkPos pos, int references, PiecesContainer children) { + this.structure = structure; + this.chunkPos = pos; +- this.references = references; ++ this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading + this.pieceContainer = children; + } + +@@ -137,7 +137,7 @@ public final class StructureStart { + nbttagcompound.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); + nbttagcompound.putInt("ChunkX", chunkPos.x); + nbttagcompound.putInt("ChunkZ", chunkPos.z); +- nbttagcompound.putInt("references", this.references); ++ nbttagcompound.putInt("references", this.references.get()); // Folia - region threading + nbttagcompound.put("Children", this.pieceContainer.save(context)); + return nbttagcompound; + } else { +@@ -155,15 +155,29 @@ public final class StructureStart { + } + + public boolean canBeReferenced() { +- return this.references < this.getMaxReferences(); ++ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading + } + ++ // Folia start - region threading ++ public boolean tryReference() { ++ for (int curr = this.references.get();;) { ++ if (curr >= this.getMaxReferences()) { ++ return false; ++ } ++ ++ if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) { ++ return true; ++ } // else: try again ++ } ++ } ++ // Folia end - region threading ++ + public void addReference() { +- ++this.references; ++ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading + } + + public int getReferences() { +- return this.references; ++ return this.references.get(); // Folia - region threading + } + + protected int getMaxReferences() { +diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +index 3a95e3236eafd14baed035e53503b58c2e21b68a..63b12dcb1b86e15607ebbaa157d7a330c089862d 100644 +--- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java ++++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +@@ -49,6 +49,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + } + + private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading + boolean bl = this.count > 0; + boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; + this.count++; +diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +index d114c62c4b0157e14fbdedebf675a53c401710ad..20da1447e036055d4c2748cc90eef15b4880e6b3 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +@@ -8,7 +8,7 @@ import net.minecraft.nbt.NbtUtils; + import net.minecraft.util.datafix.DataFixTypes; + + public abstract class SavedData { +- private boolean dirty; ++ private volatile boolean dirty; // Folia - make map data thread-safe + + public abstract CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries); + +@@ -26,9 +26,10 @@ public abstract class SavedData { + + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag compoundTag = new CompoundTag(); ++ this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost + compoundTag.put("data", this.save(new CompoundTag(), registries)); + NbtUtils.addCurrentDataVersion(compoundTag); +- this.setDirty(false); ++ // Folia - make map data thread-safe - move before save, so that any changes after are not lost + return compoundTag; + } + +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java +index 84f4664950e0cef7bd823bfc74f37cefce620d9e..8fb853d69691d7254abe6894a77270e844e3d9b8 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java +@@ -34,17 +34,21 @@ public class MapIndex extends SavedData { + + @Override + public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries) { ++ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe + for (Entry entry : this.usedAuxIds.object2IntEntrySet()) { + nbt.putInt(entry.getKey(), entry.getIntValue()); + } ++ } // Folia - make map data thread-safe + + return nbt; + } + + public MapId getFreeAuxValueForMap() { ++ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe + int i = this.usedAuxIds.getInt("map") + 1; + this.usedAuxIds.put("map", i); + this.setDirty(); + return new MapId(i); ++ } // Folia - make map data thread-safe + } + } +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 ae321b3b8d98e42ef07fd1f0f738c1a2b428f6db..ed258986f2bdec9f78aa41d7dc49a5ec14094c57 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 +@@ -212,7 +212,7 @@ public class MapItemSavedData extends SavedData { + } + + @Override +- public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries) { ++ public synchronized CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries) { // Folia - make map data thread-safe + DataResult dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location()); // CraftBukkit - decompile error + Logger logger = MapItemSavedData.LOGGER; + +@@ -262,7 +262,7 @@ public class MapItemSavedData extends SavedData { + return nbt; + } + +- public MapItemSavedData locked() { ++ public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe + MapItemSavedData worldmap = new MapItemSavedData(this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension); + + worldmap.bannerMarkers.putAll(this.bannerMarkers); +@@ -272,7 +272,7 @@ public class MapItemSavedData extends SavedData { + return worldmap; + } + +- public MapItemSavedData scaled() { ++ public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe + return MapItemSavedData.createFresh((double) this.centerX, (double) this.centerZ, (byte) Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); + } + +@@ -284,7 +284,8 @@ public class MapItemSavedData extends SavedData { + }; + } + +- public void tickCarriedBy(Player player, ItemStack stack) { ++ public synchronized void tickCarriedBy(Player player, ItemStack stack) { // Folia - make map data thread-safe ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading + if (!this.carriedByPlayers.containsKey(player)) { + MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = new MapItemSavedData.HoldingPlayer(player); + +@@ -439,7 +440,7 @@ public class MapItemSavedData extends SavedData { + + private byte calculateRotation(@Nullable LevelAccessor world, double rotation) { + if (this.dimension == Level.NETHER && world != null) { +- int i = (int) (world.getLevelData().getDayTime() / 10L); ++ int i = (int) (world.dayTime() / 10L); // Folia - region threading + + return (byte) (i * i * 34187121 + i * 121 >> 15 & 15); + } else { +@@ -470,14 +471,14 @@ public class MapItemSavedData extends SavedData { + } + + @Nullable +- public Packet getUpdatePacket(MapId mapId, Player player) { ++ public synchronized Packet getUpdatePacket(MapId mapId, Player player) { // Folia - make map data thread-safe + MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); + + return worldmap_worldmaphumantracker == null ? null : worldmap_worldmaphumantracker.nextUpdatePacket(mapId); + } + +- public void setColorsDirty(int x, int z) { +- this.setDirty(); ++ public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe ++ // Folia - make dirty only after updating data - moved down + Iterator iterator = this.carriedBy.iterator(); + + while (iterator.hasNext()) { +@@ -485,15 +486,16 @@ public class MapItemSavedData extends SavedData { + + worldmap_worldmaphumantracker.markColorsDirty(x, z); + } +- ++ this.setDirty(); // Folia - make dirty only after updating data - moved from above + } + +- public void setDecorationsDirty() { +- this.setDirty(); ++ public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe ++ // Folia - make dirty only after updating data - moved down + this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); ++ this.setDirty(); // Folia - make dirty only after updating data - moved from above + } + +- public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { ++ public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe + MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); + + if (worldmap_worldmaphumantracker == null) { +@@ -505,7 +507,7 @@ public class MapItemSavedData extends SavedData { + return worldmap_worldmaphumantracker; + } + +- public boolean toggleBanner(LevelAccessor world, BlockPos pos) { ++ public synchronized boolean toggleBanner(LevelAccessor world, BlockPos pos) { // Folia - make map data thread-safe + double d0 = (double) pos.getX() + 0.5D; + double d1 = (double) pos.getZ() + 0.5D; + int i = 1 << this.scale; +@@ -514,7 +516,7 @@ public class MapItemSavedData extends SavedData { + boolean flag = true; + + if (d2 >= -63.0D && d3 >= -63.0D && d2 <= 63.0D && d3 <= 63.0D) { +- MapBanner mapiconbanner = MapBanner.fromWorld(world, pos); ++ MapBanner mapiconbanner = world.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(world, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own + + if (mapiconbanner == null) { + return false; +@@ -535,7 +537,7 @@ public class MapItemSavedData extends SavedData { + return false; + } + +- public void checkBanners(BlockGetter world, int x, int z) { ++ public synchronized void checkBanners(BlockGetter world, int x, int z) { // Folia - make map data thread-safe + Iterator iterator = this.bannerMarkers.values().iterator(); + + while (iterator.hasNext()) { +@@ -557,13 +559,13 @@ public class MapItemSavedData extends SavedData { + return this.bannerMarkers.values(); + } + +- public void removedFromFrame(BlockPos pos, int id) { ++ public synchronized void removedFromFrame(BlockPos pos, int id) { // Folia - make map data thread-safe + this.removeDecoration(MapItemSavedData.getFrameKey(id)); + this.frameMarkers.remove(MapFrame.frameId(pos)); + this.setDirty(); + } + +- public boolean updateColor(int x, int z, byte color) { ++ public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe + byte b1 = this.colors[x + z * 128]; + + if (b1 != color) { +@@ -574,12 +576,12 @@ public class MapItemSavedData extends SavedData { + } + } + +- public void setColor(int x, int z, byte color) { ++ public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe + this.colors[x + z * 128] = color; + this.setColorsDirty(x, z); + } + +- public boolean isExplorationMap() { ++ public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe + Iterator iterator = this.decorations.values().iterator(); + + MapDecoration mapicon; +@@ -595,7 +597,7 @@ public class MapItemSavedData extends SavedData { + return true; + } + +- public void addClientSideDecorations(List decorations) { ++ public synchronized void addClientSideDecorations(List decorations) { // Folia - make map data thread-safe + this.decorations.clear(); + this.trackedDecorationCount = 0; + +@@ -614,7 +616,7 @@ public class MapItemSavedData extends SavedData { + return this.decorations.values(); + } + +- public boolean isTrackedCountOverLimit(int decorationCount) { ++ public synchronized boolean isTrackedCountOverLimit(int decorationCount) { // Folia - make map data thread-safe + return this.trackedDecorationCount >= decorationCount; + } + +@@ -766,11 +768,13 @@ public class MapItemSavedData extends SavedData { + } + + public void applyToMap(MapItemSavedData mapState) { ++ synchronized (mapState) { // Folia - make map data thread-safe + for (int i = 0; i < this.width; ++i) { + for (int j = 0; j < this.height; ++j) { + mapState.setColor(this.startX + i, this.startY + j, this.mapColors[i + j * this.width]); + } + } ++ } // Folia - make map data thread-safe + + } + } +diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +index 9272c91e0ee489091fdf1fedcf3801c070e9e82a..188aac988659e8ee36d0990acb001868ba398a3c 100644 +--- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java ++++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +@@ -51,6 +51,7 @@ public class DimensionDataStorage implements AutoCloseable { + } + + public T computeIfAbsent(SavedData.Factory type, String id) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + T savedData = this.get(type, id); + if (savedData != null) { + return savedData; +@@ -59,10 +60,12 @@ public class DimensionDataStorage implements AutoCloseable { + this.set(id, savedData2); + return savedData2; + } ++ } // Folia - make map data thread-safe + } + + @Nullable + public T get(SavedData.Factory type, String id) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + Optional optional = this.cache.get(id); + if (optional == null) { + optional = Optional.ofNullable(this.readSavedData(type.deserializer(), type.type(), id)); +@@ -70,6 +73,7 @@ public class DimensionDataStorage implements AutoCloseable { + } + + return (T)optional.orElse(null); ++ } // Folia - make map data thread-safe + } + + @Nullable +@@ -88,8 +92,10 @@ public class DimensionDataStorage implements AutoCloseable { + } + + public void set(String id, SavedData state) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + this.cache.put(id, Optional.of(state)); + state.setDirty(); ++ } // Folia - make map data thread-safe + } + + public CompoundTag readTagFromDisk(String id, DataFixTypes dataFixTypes, int currentSaveVersion) throws IOException { +diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +index 3858c83c58e78435a6e29de84c33faa2f26d593d..4af07bd98b6bdb056b2e1138a9f9fc3dada725df 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -48,6 +48,21 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + this.dirty = false; + } + // Paper end - rewrite chunk system ++ // Folia start - region threading ++ public void offsetTicks(final long offset) { ++ if (offset == 0 || this.tickQueue.isEmpty()) { ++ return; ++ } ++ final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); ++ this.tickQueue.clear(); ++ for (final ScheduledTick entry : queue) { ++ final ScheduledTick newEntry = new ScheduledTick<>( ++ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() ++ ); ++ this.tickQueue.add(newEntry); ++ } ++ } ++ // Folia end - region threading + + public LevelChunkTicks() { + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +index 778e6476c86d823dc8efe603a95e589e8b2ea9d9..3fc6b4f93885fe447ed068bc5e0784daad655696 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +@@ -38,12 +38,69 @@ public class LevelTicks implements LevelTickAccess { + private final List> alreadyRunThisTick = new ArrayList<>(); + private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); + private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (chunkTickScheduler, tick) -> { +- if (tick.equals(chunkTickScheduler.peek())) { +- this.updateContainerScheduling(tick); ++ if (tick.equals(chunkTickScheduler.peek())) { // Folia - diff on change ++ this.updateContainerScheduling(tick); // Folia - diff on change + } + }; + +- public LevelTicks(LongPredicate tickingFutureReadyPredicate) { ++ // Folia start - region threading ++ public final net.minecraft.server.level.ServerLevel world; ++ public final boolean isBlock; ++ ++ public void merge(final LevelTicks into, final long tickOffset) { ++ // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet ++ // are all transient state, only ever non-empty during tick. But merging regions occurs while there ++ // is no tick happening, so we assume they are empty. ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final LevelChunkTicks tickContainer = entry.getValue(); ++ tickContainer.offsetTicks(tickOffset); ++ into.allContainers.put(entry.getLongKey(), tickContainer); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); ++ } ++ } ++ ++ public void split(final int chunkToRegionShift, ++ final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); ++ } ++ } ++ // Folia end - region threading ++ ++ public LevelTicks(LongPredicate tickingFutureReadyPredicate, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock + this.tickCheck = tickingFutureReadyPredicate; + } + +@@ -55,7 +112,17 @@ public class LevelTicks implements LevelTickAccess { + this.nextTickForContainer.put(l, scheduledTick.triggerTick()); + } + +- scheduler.setOnTickAdded(this.chunkScheduleUpdater); ++ // Folia start - region threading ++ final boolean isBlock = this.isBlock; ++ final net.minecraft.server.level.ServerLevel world = this.world; ++ // make sure the lambda contains no reference to this LevelTicks ++ scheduler.setOnTickAdded((chunkTickScheduler, tick) -> { ++ if (tick.equals(chunkTickScheduler.peek())) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); ++ (isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks()).updateContainerScheduling((ScheduledTick)tick); ++ } ++ }); ++ // Folia end - region threading + } + + public void removeContainer(ChunkPos pos) { +@@ -69,6 +136,7 @@ public class LevelTicks implements LevelTickAccess { + + @Override + public void schedule(ScheduledTick orderedTick) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, orderedTick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading + long l = ChunkPos.asLong(orderedTick.pos()); + LevelChunkTicks levelChunkTicks = this.allContainers.get(l); + if (levelChunkTicks == null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 97b5d6ba2b19a7c730730c74175a29157aed1840..26e1584557c8ba7b6bdf4a5ca7fc801d2f33fbdf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -315,7 +315,7 @@ public final class CraftServer implements Server { + public final io.papermc.paper.SparksFly spark; // Paper - spark + + // Paper start - Folia region threading API +- private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); ++ private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading + private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler(); + +@@ -392,7 +392,7 @@ public final class CraftServer implements Server { + + @Override + public final boolean isGlobalTickThread() { +- return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); ++ return io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread(); // Folia - region threading API + } + // Paper end - Folia reagion threading API + +@@ -987,6 +987,9 @@ public final class CraftServer implements Server { + + // NOTE: Should only be called from DedicatedServer.ah() + public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("May not dispatch server commands async"); ++ // Folia end - region threading + if (sender instanceof Conversable) { + Conversable conversable = (Conversable) sender; + +@@ -1006,12 +1009,46 @@ public final class CraftServer implements Server { + } + } + ++ // Folia start - region threading ++ public void dispatchCmdAsync(CommandSender sender, String commandLine) { ++ if ((sender instanceof Entity entity)) { ++ ((org.bukkit.craftbukkit.entity.CraftEntity)entity).taskScheduler.schedule( ++ (nmsEntity) -> { ++ CraftServer.this.dispatchCommand(nmsEntity.getBukkitEntity(), commandLine); ++ }, ++ null, ++ 1L ++ ); ++ } else if (sender instanceof ConsoleCommandSender || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ CraftServer.this.dispatchCommand(sender, commandLine); ++ }); ++ } else { ++ // huh? ++ throw new UnsupportedOperationException("Dispatching command for " + sender); ++ } ++ } ++ // Folia end - region threading ++ + @Override + public boolean dispatchCommand(CommandSender sender, String commandLine) { + Preconditions.checkArgument(sender != null, "sender cannot be null"); + Preconditions.checkArgument(commandLine != null, "commandLine cannot be null"); + org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + commandLine); // Spigot // Paper - Include command in error message + ++ // Folia start - region threading ++ if ((sender instanceof Entity entity)) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle(), "Dispatching command async"); ++ } else if (sender instanceof ConsoleCommandSender || sender instanceof net.minecraft.server.rcon.RconConsoleSource ++ || sender instanceof org.bukkit.craftbukkit.command.CraftRemoteConsoleCommandSender ++ || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Dispatching command async"); ++ } else { ++ // huh? ++ throw new UnsupportedOperationException("Dispatching command for " + sender); ++ } ++ // Folia end - region threading ++ + if (this.commandMap.dispatch(sender, commandLine)) { + return true; + } +@@ -3234,7 +3271,7 @@ public final class CraftServer implements Server { + + @Override + public int getCurrentTick() { +- return net.minecraft.server.MinecraftServer.currentTick; ++ return (int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 92d9f0ea8f7810ae20d3996f49aefa539b4bcb69..f5412bdb4e80218de13b6beb62a50181fa2c3271 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -233,7 +233,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getTickableTileEntityCount() { +- return world.blockEntityTickers.size(); ++ throw new UnsupportedOperationException(); // Folia - region threading - TODO fix this? + } + + @Override +@@ -300,7 +300,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + // Paper start - per world spawn limits + for (SpawnCategory spawnCategory : SpawnCategory.values()) { + if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { +- setSpawnLimit(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); ++ this.spawnCategoryLimit.put(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); // Folia - region threading + } + } + // Paper end - per world spawn limits +@@ -370,6 +370,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public Chunk getChunkAt(int x, int z) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "Async chunk retrieval"); // Folia - region threading + warnUnsafeChunk("getting a faraway chunk", x, z); // Paper + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) this.world.getChunk(x, z, ChunkStatus.FULL, true); + return new CraftChunk(chunk); +@@ -400,10 +401,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public boolean isChunkGenerated(int x, int z) { + // Paper start - Fix this method +- if (!Bukkit.isPrimaryThread()) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading + return java.util.concurrent.CompletableFuture.supplyAsync(() -> { + return CraftWorld.this.isChunkGenerated(x, z); +- }, world.getChunkSource().mainThreadProcessor).join(); ++ }, (run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.getHandle(), x, z, run);}).join(); // Folia - region threading + } + ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); + if (chunk != null) { +@@ -460,7 +461,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + + private boolean unloadChunk0(int x, int z, boolean save) { +- org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // Folia - region threading + if (!this.isChunkLoaded(x, z)) { + return true; + } +@@ -477,7 +478,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean regenerateChunk(int x, int z) { +- org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // Folia - region threading + throw new UnsupportedOperationException("Not supported in this Minecraft version! Unless you can fix it, this is not a bug :)"); + /* + if (!unloadChunk0(x, z, false)) { +@@ -504,6 +505,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean refreshChunk(int x, int z) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // Folia - region threading + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +@@ -553,7 +555,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean loadChunk(int x, int z, boolean generate) { +- org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading + warnUnsafeChunk("loading a faraway chunk", x, z); // Paper + ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper + +@@ -594,7 +596,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; + + if (chunkDistanceManager.addRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin)) { // keep in-line with force loading, add at level 31 +- this.getChunkAt(x, z); // ensure loaded ++ //this.getChunkAt(x, z); // ensure loaded // Folia - region threading - do not load chunks for tickets anymore to make this mt-safe + return true; + } + +@@ -805,13 +807,15 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { +- this.world.captureTreeGeneration = true; +- this.world.captureBlockStates = true; ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // Folia - region threading ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading ++ worldData.captureTreeGeneration = true; // Folia - region threading ++ worldData.captureBlockStates = true; // Folia - region threading + boolean grownTree = this.generateTree(loc, type); +- this.world.captureBlockStates = false; +- this.world.captureTreeGeneration = false; ++ worldData.captureBlockStates = false; // Folia - region threading ++ worldData.captureTreeGeneration = false; // Folia - region threading + if (grownTree) { // Copy block data to delegate +- for (BlockState blockstate : this.world.capturedBlockStates.values()) { ++ for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Folia - region threading + BlockPos position = ((CraftBlockState) blockstate).getPosition(); + net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position); + int flag = ((CraftBlockState) blockstate).getFlag(); +@@ -819,10 +823,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { + net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position); + this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flag, 512); + } +- this.world.capturedBlockStates.clear(); ++ worldData.capturedBlockStates.clear(); // Folia - region threading + return true; + } else { +- this.world.capturedBlockStates.clear(); ++ worldData.capturedBlockStates.clear(); // Folia - region threading + return false; + } + } +@@ -856,6 +860,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setTime(long time) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading + long margin = (time - this.getFullTime()) % 24000; + if (margin < 0) margin += 24000; + this.setFullTime(this.getFullTime() + margin); +@@ -868,6 +873,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setFullTime(long time) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading + // Notify anyone who's listening + TimeSkipEvent event = new TimeSkipEvent(this, TimeSkipEvent.SkipReason.CUSTOM, time - this.world.getDayTime()); + this.server.getPluginManager().callEvent(event); +@@ -895,7 +901,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public long getGameTime() { +- return this.world.levelData.getGameTime(); ++ return this.getHandle().getGameTime(); // Folia - region threading + } + + @Override +@@ -920,6 +926,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source, Consumer configurator) { + // Paper end - expand explosion API ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // Folia - region threading + net.minecraft.world.level.Level.ExplosionInteraction explosionType; + if (!breakBlocks) { + explosionType = net.minecraft.world.level.Level.ExplosionInteraction.NONE; // Don't break blocks +@@ -929,6 +936,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + explosionType = net.minecraft.world.level.Level.ExplosionInteraction.MOB; // Respect mobGriefing gamerule + } + ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // Folia - region threading + net.minecraft.world.entity.Entity entity = (source == null) ? null : ((CraftEntity) source).getHandle(); + return !this.world.explode0(entity, Explosion.getDefaultDamageSource(this.world, entity), null, x, y, z, power, setFire, explosionType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE, configurator).wasCanceled; // Paper - expand explosion API + } +@@ -1011,6 +1019,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // Folia - region threading + warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper + // Transient load for this tick + return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z); +@@ -1041,6 +1050,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public void setBiome(int x, int y, int z, Holder bb) { + BlockPos pos = new BlockPos(x, 0, z); ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // Folia - region threading + if (this.world.hasChunkAt(pos)) { + net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos); + +@@ -1351,6 +1361,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setStorm(boolean hasStorm) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading + this.world.serverLevelData.setRaining(hasStorm, org.bukkit.event.weather.WeatherChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents + this.setWeatherDuration(0); // Reset weather duration (legacy behaviour) + this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) +@@ -1363,6 +1374,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setWeatherDuration(int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading + this.world.serverLevelData.setRainTime(duration); + } + +@@ -1373,6 +1385,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setThundering(boolean thundering) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading + this.world.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents + this.setThunderDuration(0); // Reset weather duration (legacy behaviour) + this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) +@@ -1385,6 +1398,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setThunderDuration(int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading + this.world.serverLevelData.setThunderTime(duration); + } + +@@ -1395,6 +1409,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setClearWeatherDuration(int duration) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading + this.world.serverLevelData.setClearWeatherTime(duration); + } + +@@ -1593,6 +1608,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setKeepSpawnInMemory(boolean keepLoaded) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify keep spawn in memory off of the global region"); // Folia - region threading + if (keepLoaded) { + this.setGameRule(GameRule.SPAWN_CHUNK_RADIUS, this.getGameRuleDefault(GameRule.SPAWN_CHUNK_RADIUS)); + } else { +@@ -1661,6 +1677,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setHardcore(boolean hardcore) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.world.serverLevelData.settings.hardcore = hardcore; + } + +@@ -1673,6 +1690,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.ANIMAL, ticksPerAnimalSpawns); + } + +@@ -1685,6 +1703,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.MONSTER, ticksPerMonsterSpawns); + } + +@@ -1697,6 +1716,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.WATER_ANIMAL, ticksPerWaterSpawns); + } + +@@ -1709,6 +1729,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerWaterAmbientSpawns(int ticksPerWaterAmbientSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.WATER_AMBIENT, ticksPerWaterAmbientSpawns); + } + +@@ -1721,6 +1742,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerWaterUndergroundCreatureSpawns(int ticksPerWaterUndergroundCreatureSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.WATER_UNDERGROUND_CREATURE, ticksPerWaterUndergroundCreatureSpawns); + } + +@@ -1733,11 +1755,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setTicksPerSpawns(SpawnCategory.AMBIENT, ticksPerAmbientSpawns); + } + + @Override + public void setTicksPerSpawns(SpawnCategory spawnCategory, int ticksPerCategorySpawn) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null"); + Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory); + +@@ -1754,21 +1778,25 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading + this.server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue); + } + + @Override + public List getMetadata(String metadataKey) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading + return this.server.getWorldMetadata().getMetadata(this, metadataKey); + } + + @Override + public boolean hasMetadata(String metadataKey) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading + return this.server.getWorldMetadata().hasMetadata(this, metadataKey); + } + + @Override + public void removeMetadata(String metadataKey, Plugin owningPlugin) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading + this.server.getWorldMetadata().removeMetadata(this, metadataKey, owningPlugin); + } + +@@ -1781,6 +1809,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setMonsterSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.MONSTER, limit); + } + +@@ -1793,6 +1822,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setAnimalSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.ANIMAL, limit); + } + +@@ -1805,6 +1835,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setWaterAnimalSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.WATER_ANIMAL, limit); + } + +@@ -1817,6 +1848,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setWaterAmbientSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.WATER_AMBIENT, limit); + } + +@@ -1829,6 +1861,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setWaterUndergroundCreatureSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.WATER_UNDERGROUND_CREATURE, limit); + } + +@@ -1841,6 +1874,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + @Deprecated + public void setAmbientSpawnLimit(int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + this.setSpawnLimit(SpawnCategory.AMBIENT, limit); + } + +@@ -1863,6 +1897,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setSpawnLimit(SpawnCategory spawnCategory, int limit) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null"); + Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory); + +@@ -1945,7 +1980,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; + + ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(CraftSound.bukkitToMinecraftHolder(sound), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed); +- ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading + if (entityTracker != null) { + entityTracker.broadcastAndSend(packet); + } +@@ -1966,7 +2001,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; + + ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(Holder.direct(SoundEvent.createVariableRangeEvent(ResourceLocation.parse(sound))), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed); +- ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = ((CraftEntity)entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading + if (entityTracker != null) { + entityTracker.broadcastAndSend(packet); + } +@@ -2049,6 +2084,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean setGameRuleValue(String rule, String value) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + // No null values allowed + if (rule == null || value == null) return false; + +@@ -2091,6 +2127,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean setGameRule(GameRule rule, T newValue) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading + Preconditions.checkArgument(rule != null, "GameRule cannot be null"); + Preconditions.checkArgument(newValue != null, "GameRule value cannot be null"); + +@@ -2317,6 +2354,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { ++ // Folia start - region threading ++ if (sourceEntity != null && !Bukkit.isOwnedByCurrentRegion(sourceEntity)) { ++ throw new IllegalStateException("Cannot send game event asynchronously"); ++ } ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously"); ++ // Folia end - region threading + getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())).orElseThrow(), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); + } + // Paper end +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index 5cb69d0b822e11a99a96aef4f59986d083b079f4..a2f35f6d057b098a016a40094d84c54cb5e174fd 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -75,6 +75,11 @@ public class CraftBlock implements Block { + } + + public net.minecraft.world.level.block.state.BlockState getNMS() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + return this.world.getBlockState(this.position); + } + +@@ -157,6 +162,11 @@ public class CraftBlock implements Block { + } + + private void setData(final byte data, int flag) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flag); + } + +@@ -198,6 +208,11 @@ public class CraftBlock implements Block { + } + + public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) { ++ // Folia start - region threading ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // Folia end - region threading + // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in tile entity cleanup + if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes + // SPIGOT-4612: faster - just clear tile +@@ -343,18 +358,33 @@ public class CraftBlock implements Block { + + @Override + public Biome getBiome() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); + } + + // Paper start + @Override + public Biome getComputedBiome() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); + } + // Paper end + + @Override + public void setBiome(Biome bio) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); + } + +@@ -402,6 +432,11 @@ public class CraftBlock implements Block { + + @Override + public boolean isBlockFaceIndirectlyPowered(BlockFace face) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face)); + + Block relative = this.getRelative(face); +@@ -414,6 +449,11 @@ public class CraftBlock implements Block { + + @Override + public int getBlockPower(BlockFace face) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + int power = 0; + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); + int x = this.getX(); +@@ -500,6 +540,11 @@ public class CraftBlock implements Block { + + @Override + public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + // Paper end + // Order matters here, need to drop before setting to air so skulls can get their data + net.minecraft.world.level.block.state.BlockState iblockdata = this.getNMS(); +@@ -543,21 +588,27 @@ public class CraftBlock implements Block { + + @Override + public boolean applyBoneMeal(BlockFace face) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + Direction direction = CraftBlock.blockFaceToNotch(face); + BlockFertilizeEvent event = null; + ServerLevel world = this.getCraftWorld().getHandle(); + UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); + ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Folia - region threading + InteractionResult result = BoneMealItem.applyBonemeal(context); +- world.captureTreeGeneration = false; ++ worldData.captureTreeGeneration = false; // Folia - region threading + +- if (world.capturedBlockStates.size() > 0) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; +- List blocks = new ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ SaplingBlock.treeTypeRT.set(null); // Folia - region threading ++ List blocks = new ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + StructureGrowEvent structureEvent = null; + + if (treeType != null) { +@@ -644,6 +695,11 @@ public class CraftBlock implements Block { + + @Override + public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + Preconditions.checkArgument(start != null, "Location start cannot be null"); + Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world"); + start.checkFinite(); +@@ -685,6 +741,11 @@ public class CraftBlock implements Block { + + @Override + public boolean canPlace(BlockData data) { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + Preconditions.checkArgument(data != null, "BlockData cannot be null"); + net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState(); + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); +@@ -719,18 +780,32 @@ public class CraftBlock implements Block { + + @Override + public void tick() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + final ServerLevel level = this.world.getMinecraftWorld(); + this.getNMS().tick(level, this.position, level.random); + } + +- + @Override + public void fluidTick() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + this.getNMSFluid().tick(this.world.getMinecraftWorld(), this.position, this.getNMS()); + } + + @Override + public void randomTick() { ++ // Folia start - region threading ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // Folia end - region threading + final ServerLevel level = this.world.getMinecraftWorld(); + this.getNMS().randomTick(level, this.position, level.random); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +index 04ae258a2f8e98421340d29d5cceedec045171b7..698a87ac30cc9efabeef3344ee254bcace1256c9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +@@ -25,7 +25,7 @@ public abstract class CraftBlockEntityState extends Craft + private final T tileEntity; + private final T snapshot; + public boolean snapshotDisabled; // Paper +- public static boolean DISABLE_SNAPSHOT = false; // Paper ++ public static final ThreadLocal DISABLE_SNAPSHOT = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper // Folia - region threading + + public CraftBlockEntityState(World world, T tileEntity) { + super(world, tileEntity.getBlockPos(), tileEntity.getBlockState()); +@@ -34,8 +34,8 @@ public abstract class CraftBlockEntityState extends Craft + + try { // Paper - Show blockstate location if we failed to read it + // Paper start +- this.snapshotDisabled = DISABLE_SNAPSHOT; +- if (DISABLE_SNAPSHOT) { ++ this.snapshotDisabled = DISABLE_SNAPSHOT.get().booleanValue(); // Folia - region threading ++ if (this.snapshotDisabled) { // Folia - region threading + this.snapshot = this.tileEntity; + } else { + this.snapshot = this.createSnapshot(tileEntity); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +index fa63a6cfcfcc4eee4503a82d85333c139c8c8b2b..def7749e6dc4ae8351b72deefc75936629c33d7f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +@@ -215,6 +215,12 @@ public class CraftBlockState implements BlockState { + LevelAccessor access = this.getWorldHandle(); + CraftBlock block = this.getBlock(); + ++ // Folia start - region threading ++ if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // Folia end - region threading ++ + if (block.getType() != this.getType()) { + if (!force) { + return false; +@@ -350,6 +356,9 @@ public class CraftBlockState implements BlockState { + + @Override + public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { ++ // Folia start - region threading ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); ++ // Folia end - region threading + this.requirePlaced(); + net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +index 56453454cbd4b9e9270fc833f8ab38d5fa7a3763..69e8a170a80c2fde79bc015cd54879896c110d9d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +@@ -249,8 +249,8 @@ public final class CraftBlockStates { + net.minecraft.world.level.block.state.BlockState blockData = craftBlock.getNMS(); + BlockEntity tileEntity = craftBlock.getHandle().getBlockEntity(blockPosition); + // Paper start - block state snapshots +- boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT; +- CraftBlockEntityState.DISABLE_SNAPSHOT = !useSnapshot; ++ boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT.get().booleanValue(); // Folia - region threading ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(Boolean.valueOf(!useSnapshot)); // Folia - region threading + try { + // Paper end + CraftBlockState blockState = CraftBlockStates.getBlockState(world, blockPosition, blockData, tileEntity); +@@ -258,7 +258,7 @@ public final class CraftBlockStates { + return blockState; + // Paper start + } finally { +- CraftBlockEntityState.DISABLE_SNAPSHOT = prev; ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(Boolean.valueOf(prev)); // Folia - region threading + } + // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +index a45e658996e483e9a21cfd8178153ddb7b87ae69..25303f144422469350fdc6f84320b16bcc9f6e0c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +@@ -50,7 +50,7 @@ public class ConsoleCommandCompleter implements Completer { + return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of(); + } + }; +- server.getServer().processQueue.add(syncCompletions); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(syncCompletions); // Folia - region threading + try { + final List legacyCompletions = syncCompletions.get(); + completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed +@@ -98,7 +98,7 @@ public class ConsoleCommandCompleter implements Completer { + return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); + } + }; +- server.getServer().processQueue.add(waitable); // Paper - Remove "this." ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(waitable); // Folia - region threading + try { + List offers = waitable.get(); + if (offers == null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index b25b10c24a379097233e61bcc10add841b6a7115..5168cf0d58013aecfd80d37fb698014f38f8e08d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -80,6 +80,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return this.apiScheduler; + }; + // Paper end - Folia schedulers ++ // Folia start - region threading ++ public boolean isPurged() { ++ return this.taskScheduler.isRetired(); ++ } ++ // Folia end - region threading + + public CraftEntity(final CraftServer server, final Entity entity) { + this.server = server; +@@ -237,6 +242,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + + @Override + public boolean teleport(Location location, TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + // Paper end + Preconditions.checkArgument(location != null, "location cannot be null"); + location.checkFinite(); +@@ -698,7 +708,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + ImmutableSet.Builder players = ImmutableSet.builder(); + + ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); +- ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = this.getHandle().moonrise$getTrackedEntity(); // Folia - region threading + + if (entityTracker != null) { + for (ServerPlayerConnection connection : entityTracker.seenBy) { +@@ -1002,7 +1012,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); +- ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = this.getHandle().moonrise$getTrackedEntity(); // Folia - region threading + + if (entityTracker == null) { + return; +@@ -1021,7 +1031,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); +- ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = this.entity.moonrise$getTrackedEntity(); // Folia - region threading + + if (entityTracker == null) { + return; +@@ -1055,29 +1065,43 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + location.checkFinite(); + Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. + +- net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); ++ // Folia start - region threading + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); +- +- world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), +- this instanceof CraftPlayer ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL, (list) -> { +- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { +- final net.minecraft.server.level.ServerChunkCache chunkCache = world.getChunkSource(); +- for (final net.minecraft.world.level.chunk.ChunkAccess chunk : list) { +- chunkCache.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); +- } +- try { +- ret.complete(CraftEntity.this.teleport(locationClone, cause, teleportFlags) ? Boolean.TRUE : Boolean.FALSE); +- } catch (Throwable throwable) { +- if (throwable instanceof ThreadDeath) { +- throw (ThreadDeath)throwable; +- } +- net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); +- ret.completeExceptionally(throwable); +- } +- }); +- }); ++ java.util.function.Consumer run = (Entity nmsEntity) -> { ++ boolean success = nmsEntity.teleportAsync( ++ ((CraftWorld)locationClone.getWorld()).getHandle(), ++ new net.minecraft.world.phys.Vec3(locationClone.getX(), locationClone.getY(), locationClone.getZ()), ++ locationClone.getYaw(), locationClone.getPitch(), net.minecraft.world.phys.Vec3.ZERO, ++ cause == null ? TeleportCause.UNKNOWN : cause, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, // preserve behavior with old API: dismount the entity so it can teleport ++ (Entity entityTp) -> { ++ ret.complete(Boolean.TRUE); ++ } ++ ); ++ if (!success) { ++ ret.complete(Boolean.FALSE); ++ } ++ }; ++ if (org.bukkit.Bukkit.isOwnedByCurrentRegion(this)) { ++ run.accept(this.getHandle()); ++ return ret; ++ } ++ boolean scheduled = this.taskScheduler.schedule( ++ // success ++ run, ++ // retired ++ (Entity nmsEntity) -> { ++ ret.complete(Boolean.FALSE); ++ }, ++ 1L ++ ); ++ ++ if (!scheduled) { ++ ret.complete(Boolean.FALSE); ++ } + + return ret; ++ // Folia end - region threading + } + // Paper end - more teleport API / async chunk API + +@@ -1190,8 +1214,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + // Paper start - tracked players API + @Override + public Set getTrackedPlayers() { +- ServerLevel world = (net.minecraft.server.level.ServerLevel)this.entity.level(); +- ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.entity.getId()); ++ ChunkMap.TrackedEntity tracker = this.entity.moonrise$getTrackedEntity(); // Folia - region threading + if (tracker == null) { + return java.util.Collections.emptySet(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 6a647cab8b2e476987931486e290703b8726f2c7..f2a847e590c72eee91a053cecdc691c53751ca3a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -665,7 +665,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void kickPlayer(String message) { +- org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot ++ //org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot // Folia - thread-safe now, as it will simply delay the kick + this.getHandle().transferCookieConnection.kickPlayer(CraftChatMessage.fromStringOrEmpty(message, true), org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause + } + +@@ -1421,6 +1421,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public boolean teleport(Location location, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + Set relativeArguments; + Set allFlags; + if (flags.length == 0) { +@@ -2085,7 +2090,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + private void unregisterEntity(Entity other) { + // Paper end + ChunkMap tracker = ((ServerLevel) this.getHandle().level()).getChunkSource().chunkMap; +- ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); ++ ChunkMap.TrackedEntity entry = other.moonrise$getTrackedEntity(); // Folia - region threading + if (entry != null) { + entry.removePlayer(this.getHandle()); + } +@@ -2182,7 +2187,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + if (original != null) otherPlayer.setUUID(original); // Paper - uuid override + } + +- ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); ++ ChunkMap.TrackedEntity entry = other.moonrise$getTrackedEntity(); // Folia - region threading + if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { + entry.updatePlayer(this.getHandle()); + } +@@ -3363,7 +3368,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + { + if ( CraftPlayer.this.getHealth() <= 0 && CraftPlayer.this.isOnline() ) + { +- CraftPlayer.this.server.getServer().getPlayerList().respawn( CraftPlayer.this.getHandle(), false, Entity.RemovalReason.KILLED, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.PLUGIN ); ++ CraftPlayer.this.getHandle().respawn(null, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.PLUGIN); // Folia - region threading + } + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index e37aaf77f94b97b736cc20ef070cefdff0400188..ebbe224d81f6a96f3b05e3379cd0c5b5ab50fcbd 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -951,7 +951,7 @@ public class CraftEventFactory { + return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2); + } + +- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. ++ public static final ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading + + public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState block, int flag) { + // Suppress during worldgen +@@ -963,7 +963,7 @@ public class CraftEventFactory { + CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag); + state.setData(block); + +- BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state); ++ BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // Folia - region threading + Bukkit.getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +@@ -2229,7 +2229,7 @@ public class CraftEventFactory { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemStack.copyWithCount(1)); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), CraftVector.toBukkit(to)); +- if (!net.minecraft.world.level.block.DispenserBlock.eventFired) { ++ if (!net.minecraft.world.level.block.DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + if (!event.callEvent()) { + return itemStack; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index 1354ccfbf525e5e64483ac5f443cc2325ba63850..fad85bea8643a3a88ec5c4194de7a5060e81c136 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -514,6 +514,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + protected CraftTask handle(final CraftTask task, final long delay) { // Paper ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + // Paper start + if (!this.isAsyncScheduler && !task.isSync()) { + this.asyncScheduler.handle(task, delay); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index 37458e8fd5d57acbf90a6bea4e66797cb07f69fa..7b572e0b730ba989c5df62dcef458e5ead507870 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -66,6 +66,13 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + this.handle = worldAccess; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.level.StructureManager structureManager() { ++ return this.handle.structureManager(); ++ } ++ // Folia end - region threading ++ + public WorldGenLevel getHandle() { + return this.handle; + } +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 1d438ef44cbe4d1eedfba36d8fe5d2ad53464921..f2f5eb1a443ac411539e1c87eec60e76682b82fa 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -51,7 +51,7 @@ public class ActivationRange + RAIDER, + MISC; + +- AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 ); ++ // Folia - threaded regions - replaced by local variable + } + // Paper start + +@@ -64,26 +64,27 @@ public class ActivationRange + + private static int checkInactiveWakeup(Entity entity) { + Level world = entity.level(); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions + SpigotWorldConfig config = world.spigotConfig; +- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions + if (entity.activationType == ActivationType.VILLAGER) { +- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { +- world.wakeupInactiveRemainingVillagers--; ++ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions ++ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions + return config.wakeUpInactiveVillagersFor; + } + } else if (entity.activationType == ActivationType.ANIMAL) { +- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { +- world.wakeupInactiveRemainingAnimals--; ++ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions ++ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions + return config.wakeUpInactiveAnimalsFor; + } + } else if (entity.activationType == ActivationType.FLYING_MONSTER) { +- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { +- world.wakeupInactiveRemainingFlying--; ++ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions ++ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions + return config.wakeUpInactiveFlyingFor; + } + } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { +- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { +- world.wakeupInactiveRemainingMonsters--; ++ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions ++ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions + return config.wakeUpInactiveMonstersFor; + } + } +@@ -91,7 +92,7 @@ public class ActivationRange + } + // Paper end + +- static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 ); ++ // Folia - threaded regions - replaced by local variable + + /** + * Initializes an entities type on construction to specify what group this +@@ -174,10 +175,11 @@ public class ActivationRange + 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); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions ++ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions ++ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions ++ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions ++ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions + final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); + // Paper end + +@@ -191,9 +193,9 @@ public class ActivationRange + // Paper end + maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); + +- for ( Player player : world.players() ) ++ for ( Player player : world.getLocalPlayers() ) // Folia - region threading + { +- player.activatedTick = MinecraftServer.currentTick; ++ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading + if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() ) + { + continue; +@@ -201,26 +203,33 @@ public class ActivationRange + + // 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 ); ++ AABB maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); // Folia - threaded regions ++ AABB[] bbByType = new AABB[ActivationType.values().length]; ++ bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange ); // Folia - threaded regions ++ bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange ); // Folia - threaded regions + // Paper end + + // Paper start +- java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null); ++ java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later ++ ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later + boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking + for (Entity entity : entities) { + // Paper start - Configurable marker ticking ++ // Folia start - region ticking ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ continue; ++ } ++ // Folia end - region ticking + if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) { + continue; + } + // Paper end - Configurable marker ticking +- ActivationRange.activateEntity(entity); ++ ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions + } + // Paper end + } +@@ -231,18 +240,18 @@ public class ActivationRange + * + * @param chunk + */ +- private static void activateEntity(Entity entity) ++ private static void activateEntity(Entity entity, AABB[] bbByType) // Folia - threaded regions + { +- if ( MinecraftServer.currentTick > entity.activatedTick ) ++ if ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick ) // Folia - threaded regions + { + if ( entity.defaultActivationState ) + { +- entity.activatedTick = MinecraftServer.currentTick; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions + return; + } +- if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) ++ if ( bbByType[entity.activationType.ordinal()].intersects( entity.getBoundingBox() ) ) // Folia - threaded regions + { +- entity.activatedTick = MinecraftServer.currentTick; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions + } + } + } +@@ -265,10 +274,10 @@ public class ActivationRange + if (entity.getRemainingFireTicks() > 0) { + return 2; + } +- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { ++ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions + return 1; + } +- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions + // Paper end + // quick checks. + if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper +@@ -391,19 +400,19 @@ public class ActivationRange + } + // Paper end + +- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; ++ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions + entity.isTemporarilyActive = false; // Paper + + // Should this entity tick? + if ( !isActive ) + { +- if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) ++ if ( ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1 ) % 20 == 0 ) // Folia - threaded regions + { + // Check immunities every 20 ticks. + // Paper start + int immunity = checkEntityImmunities(entity); + if (immunity >= 0) { +- entity.activatedTick = MinecraftServer.currentTick + immunity; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions + } else { + entity.isTemporarilyActive = true; + } +diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java +index ac0fd418fcb437896dfdff53ab3eff19833d25fb..130220977477a5d8d51e17dcb989ae0c858643cb 100644 +--- a/src/main/java/org/spigotmc/SpigotCommand.java ++++ b/src/main/java/org/spigotmc/SpigotCommand.java +@@ -29,6 +29,7 @@ public class SpigotCommand extends Command { + Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); + Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); + ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + MinecraftServer console = MinecraftServer.getServer(); + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); + for (ServerLevel world : console.getAllLevels()) { +@@ -37,6 +38,7 @@ public class SpigotCommand extends Command { + console.server.reloadCount++; + + Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete."); ++ }); // Folia - region threading + } + + return true; +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index 4dbb109d0526afee99b9190fc256585121aac9b5..5c80e89034c1b3149729684ba4dd4ae26a4261c1 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -224,7 +224,7 @@ public class SpigotConfig + SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash ); + SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript ); + SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) ); +- SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); ++ // SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); // Folia - region threading + // WatchdogThread.doStart( SpigotConfig.timeoutTime, SpigotConfig.restartOnCrash ); // Paper - moved to after paper config initialization + } + +@@ -279,7 +279,7 @@ public class SpigotConfig + + private static void tpsCommand() + { +- SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); ++ //SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); // Folia - region threading + } + + public static int playerSample; +diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java +index 2c408fa4abcbe1171c58aee8799c8cf7867d0f0a..9f38a0763597d9d70cb8d1b636c7d4b14d32c535 100644 +--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java ++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java +@@ -435,7 +435,7 @@ public class SpigotWorldConfig + this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 ); + } + +- public int currentPrimedTnt = 0; ++ //public int currentPrimedTnt = 0; // Folia - region threading - moved to regionised world data + public int maxTntTicksPerTick; + private void maxTntPerTick() { + if ( SpigotConfig.version < 7 ) diff --git a/patches/server/0004-Max-pending-logins.patch b/patches/server/0004-Max-pending-logins.patch new file mode 100644 index 0000000..9db9e3e --- /dev/null +++ b/patches/server/0004-Max-pending-logins.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 9 Mar 2023 20:50:15 -0800 +Subject: [PATCH] Max pending logins + +Should help the floodgates on launch + +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 7fdb9304de7cf1979d57e3fac32415d7c674609d..227d62a69a453d49c28568ecb41ecef85a35405b 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -117,7 +117,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { + // Folia end - region threading - rewrite login process + this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile)); +- } // Paper - prevent logins to be processed even though disconnect was called ++ } else { --this.tick; } // Paper - prevent logins to be processed even though disconnect was called // Folia - max concurrent logins + } + + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index b449de44b1911e2ff0701956bfba53fb5d2ed44e..c1574cdea90731dec4d24b15979209cce0c581af 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -175,6 +175,17 @@ public abstract class PlayerList { + conflictingId = this.connectionById.get(byId); + + if (conflictingName == null && conflictingId == null) { ++ // Folia start - max concurrent login ++ int loggedInCount = 0; ++ for (Connection value : this.connectionById.values()) { ++ if (value.getPacketListener() instanceof ServerGamePacketListenerImpl) { ++ ++loggedInCount; ++ } ++ } ++ if ((this.connectionById.size() - loggedInCount) >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick) { ++ return false; ++ } ++ // Folia end - max concurrent login + this.connectionByName.put(userName, conn); + this.connectionById.put(byId, conn); + } diff --git a/patches/server/0005-Add-chunk-system-throughput-counters-to-tps.patch b/patches/server/0005-Add-chunk-system-throughput-counters-to-tps.patch new file mode 100644 index 0000000..0a0d89d --- /dev/null +++ b/patches/server/0005-Add-chunk-system-throughput-counters-to-tps.patch @@ -0,0 +1,86 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Fri, 10 Mar 2023 00:16:26 -0800 +Subject: [PATCH] Add chunk system throughput counters to /tps + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +index 6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2..4ad647a9f98cf1c11c45f85edcba3c29e343c236 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +@@ -30,6 +30,9 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + private final ChunkAccess fromChunk; + private final PrioritisedExecutor.PrioritisedTask convertToFullTask; + ++ public static final io.papermc.paper.util.IntervalledCounter chunkLoads = new io.papermc.paper.util.IntervalledCounter(java.util.concurrent.TimeUnit.SECONDS.toNanos(15L)); ++ public static final io.papermc.paper.util.IntervalledCounter chunkGenerates = new io.papermc.paper.util.IntervalledCounter(java.util.concurrent.TimeUnit.SECONDS.toNanos(15L)); ++ + public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) { + super(scheduler, world, chunkX, chunkZ); +@@ -43,6 +46,20 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + return ChunkStatus.FULL; + } + ++ public static double genRate(final long time) { ++ synchronized (chunkGenerates) { ++ chunkGenerates.updateCurrentTime(time); ++ return chunkGenerates.getRate(); ++ } ++ } ++ ++ public static double loadRate(final long time) { ++ synchronized (chunkLoads) { ++ chunkLoads.updateCurrentTime(time); ++ return chunkLoads.getRate(); ++ } ++ } ++ + @Override + public void run() { + final PlatformHooks platformHooks = PlatformHooks.get(); +@@ -59,6 +76,17 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk); + } + ++ final long time = System.nanoTime(); ++ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { ++ synchronized (chunkLoads) { ++ chunkLoads.updateAndAdd(1L, time); ++ } ++ } else { ++ synchronized (chunkGenerates) { ++ chunkGenerates.updateAndAdd(1L, time); ++ } ++ } ++ + if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { + chunk = wrappedFull.getWrapped(); + } else { +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +index 3bcb1dc98c61e025874cc9e008faa722581a530c..012d3a7da7fe483393a0888c823bd2e78f5c3908 100644 +--- a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +@@ -170,6 +170,9 @@ public final class CommandServerHealth extends Command { + totalUtil += (report == null ? 0.0 : report.utilisation()); + } + ++ final double genRate = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask.genRate(currTime); ++ final double loadRate = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask.loadRate(currTime); ++ + totalUtil += globalTickReport.utilisation(); + + tpsByRegion.sort(null); +@@ -284,6 +287,12 @@ public final class CommandServerHealth extends Command { + .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) + .append(Component.text("%\n", PRIMARY)) + ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Load rate: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(loadRate) + ", ", INFORMATION)) ++ .append(Component.text("Gen rate: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(genRate) + "\n", INFORMATION)) ++ + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Lowest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) diff --git a/patches/server/0006-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch b/patches/server/0006-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch new file mode 100644 index 0000000..3f68ca4 --- /dev/null +++ b/patches/server/0006-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch @@ -0,0 +1,3342 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 19 Mar 2023 14:35:46 -0700 +Subject: [PATCH] Make CraftEntity#getHandle and overrides perform thread + checks + +While these checks are painful, it should assist in debugging +threading issues for plugins. + +diff --git a/src/main/java/io/papermc/paper/entity/PaperSchoolableFish.java b/src/main/java/io/papermc/paper/entity/PaperSchoolableFish.java +index 41bf71d116ffc5431586ce54abba7f8def6c1dcf..1cf9a7677449ab8f03fb23d835e3fadce61542db 100644 +--- a/src/main/java/io/papermc/paper/entity/PaperSchoolableFish.java ++++ b/src/main/java/io/papermc/paper/entity/PaperSchoolableFish.java +@@ -11,8 +11,16 @@ public class PaperSchoolableFish extends CraftFish implements SchoolableFish { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractSchoolingFish getHandleRaw() { ++ return (AbstractSchoolingFish)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractSchoolingFish getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractSchoolingFish) super.getHandle(); + } + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 35ff983cd84cb610b70e193220a97a3a5406252f..94f2610e1f2cce41d998bb9c92abbb38d9811f56 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -3244,6 +3244,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + if (!force && (!this.canRide(entity) || !entity.canAddPassenger(this))) { + return false; + } else { ++ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen + // CraftBukkit start + if (entity.getBukkitEntity() instanceof Vehicle && this.getBukkitEntity() instanceof LivingEntity) { + VehicleEnterEvent event = new VehicleEnterEvent((Vehicle) entity.getBukkitEntity(), this.getBukkitEntity()); +@@ -3265,6 +3266,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return false; + } + // CraftBukkit end ++ } // Folia - region threading - suppress entire event logic during worldgen + if (this.isPassenger()) { + this.stopRiding(); + } +@@ -3348,6 +3350,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)"); + } else { + // CraftBukkit start ++ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen + CraftEntity craft = (CraftEntity) entity.getBukkitEntity().getVehicle(); + Entity orig = craft == null ? null : craft.getHandle(); + if (this.getBukkitEntity() instanceof Vehicle && entity.getBukkitEntity() instanceof LivingEntity) { +@@ -3375,6 +3378,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return false; + } + // CraftBukkit end ++ } // Folia - region threading - suppress entire event logic during worldgen + if (this.passengers.size() == 1 && this.passengers.get(0) == entity) { + this.passengers = ImmutableList.of(); + } else { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java +index 591af9d0d2fdc9953415979fc97a4a00afd85885..4a0fd1e3203342b7a5ffde579947057fe84a0d80 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java +@@ -1,5 +1,6 @@ + package org.bukkit.craftbukkit.entity; + ++import net.minecraft.world.entity.Entity; + import org.bukkit.craftbukkit.CraftServer; + import org.bukkit.entity.Projectile; + +@@ -38,6 +39,13 @@ public abstract class AbstractProjectile extends CraftEntity implements Projecti + this.getHandle().hasBeenShot = beenShot; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.Projectile getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.Projectile)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public boolean canHitEntity(org.bukkit.entity.Entity entity) { + return this.getHandle().canHitEntity(((CraftEntity) entity).getHandle()); +@@ -55,6 +63,7 @@ public abstract class AbstractProjectile extends CraftEntity implements Projecti + + @Override + public net.minecraft.world.entity.projectile.Projectile getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.Projectile) entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java +index d0c30fd12aa9866900fe72b97d10c257479cf010..46d8cbe8d09cf43b489d0358498e937454d96e7b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java +@@ -133,6 +133,7 @@ public class CraftAbstractArrow extends AbstractProjectile implements AbstractAr + + @Override + public net.minecraft.world.entity.projectile.AbstractArrow getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.AbstractArrow) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractHorse.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractHorse.java +index 467693a60786688b753cebac3b0a88898e332eee..5c6bd9186e47d1414c5e7bd4fa46a8e305390908 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractHorse.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractHorse.java +@@ -17,8 +17,16 @@ public abstract class CraftAbstractHorse extends CraftAnimals implements Abstrac + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.horse.AbstractHorse getHandleRaw() { ++ return (net.minecraft.world.entity.animal.horse.AbstractHorse)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.horse.AbstractHorse getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.horse.AbstractHorse) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractSkeleton.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractSkeleton.java +index 5beaa2bb0d58fe477ce8d2de8b77600d3b416d8c..c8406f2d83f4c8b60efec0de546f45760c759a2a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractSkeleton.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractSkeleton.java +@@ -15,8 +15,17 @@ public abstract class CraftAbstractSkeleton extends CraftMonster implements Abst + throw new UnsupportedOperationException("Not supported."); + } + // Paper start ++ ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.AbstractSkeleton getHandleRaw() { ++ return (net.minecraft.world.entity.monster.AbstractSkeleton)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.AbstractSkeleton getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.AbstractSkeleton) super.getHandle(); + } + // Paper end +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractVillager.java +index 3199f04d00836a0a51547c679f3f3c80d00da502..a1959919109fe04d4b829dcd2d244842ab05fe13 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractVillager.java +@@ -15,8 +15,16 @@ public class CraftAbstractVillager extends CraftAgeable implements CraftMerchant + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.npc.AbstractVillager getHandleRaw() { ++ return (net.minecraft.world.entity.npc.AbstractVillager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.npc.AbstractVillager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Villager) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractWindCharge.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractWindCharge.java +index 59df9031e8b4466c8687671d745318e7ee83d271..b91b11c2e1ed5df27e6ff99eb5cc25b931e0b79d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractWindCharge.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractWindCharge.java +@@ -17,6 +17,7 @@ public abstract class CraftAbstractWindCharge extends CraftFireball implements A + + @Override + public net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAgeable.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAgeable.java +index ae16e8d1bfe8e9315391510eddb367a3fbdc9e03..2b165c209f65de06f55ed51817e33b92463a2987 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAgeable.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAgeable.java +@@ -63,8 +63,16 @@ public class CraftAgeable extends CraftCreature implements Ageable { + } + } + ++ // Folia start - region threading ++ @Override ++ public AgeableMob getHandleRaw() { ++ return (AgeableMob)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AgeableMob getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AgeableMob) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAllay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAllay.java +index c64918175ec08d20cde2bda9e0cac8b474385fe0..0df0824d56d62f7b82fcca8f0b9a6175f012e8d3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAllay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAllay.java +@@ -16,8 +16,16 @@ public class CraftAllay extends CraftCreature implements org.bukkit.entity.Allay + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Allay getHandleRaw() { ++ return (Allay)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Allay getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Allay) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAmbient.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAmbient.java +index 2a2f9f0907eafcabef26a41d20f64a0aa953d181..9d56293083aac5c14e8333366fd4cf6148486585 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAmbient.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAmbient.java +@@ -9,8 +9,16 @@ public class CraftAmbient extends CraftMob implements Ambient { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AmbientCreature getHandleRaw() { ++ return (AmbientCreature)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AmbientCreature getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AmbientCreature) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAnimals.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAnimals.java +index ab42bc721d5b6c17c2ca6c7153b757571aea05e8..e48528689d49c01aa2b0c1599c66f3c1e94c9cd6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAnimals.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAnimals.java +@@ -15,8 +15,16 @@ public class CraftAnimals extends CraftAgeable implements Animals { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Animal getHandleRaw() { ++ return (Animal)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Animal getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Animal) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java +index f9c113dc018702159345240d6d0de85767afa0c3..0872943dc4e5895728d12289cb23682c9bef290c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java +@@ -28,8 +28,16 @@ public class CraftAreaEffectCloud extends CraftEntity implements AreaEffectCloud + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.AreaEffectCloud getHandleRaw() { ++ return (net.minecraft.world.entity.AreaEffectCloud)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.AreaEffectCloud getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.AreaEffectCloud) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftArmadillo.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftArmadillo.java +index e7f2d8de25a489d7f52c78c750e6f7f9b8fee177..75191dd32bba12b5742702a2af151b1079a6b48f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftArmadillo.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftArmadillo.java +@@ -11,6 +11,7 @@ public class CraftArmadillo extends CraftAnimals implements Armadillo { + + @Override + public net.minecraft.world.entity.animal.armadillo.Armadillo getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.armadillo.Armadillo) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftArmorStand.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftArmorStand.java +index 184fe8257e5ffb0ef090ffa2833786a4db8b59ea..6c5358f77be3e46860b0c3c49d36b25286af6851 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftArmorStand.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftArmorStand.java +@@ -20,8 +20,16 @@ public class CraftArmorStand extends CraftLivingEntity implements ArmorStand { + return "CraftArmorStand"; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.decoration.ArmorStand getHandleRaw() { ++ return (net.minecraft.world.entity.decoration.ArmorStand)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.decoration.ArmorStand getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.decoration.ArmorStand) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java +index 15d50a284cafc2eb59239ca00926836526f09e06..dc455a211005f70754f3b99213b22e85821c216f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java +@@ -26,6 +26,7 @@ public class CraftArrow extends CraftAbstractArrow implements Arrow { + + @Override + public net.minecraft.world.entity.projectile.Arrow getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.Arrow) this.entity; + } + +@@ -90,6 +91,13 @@ public class CraftArrow extends CraftAbstractArrow implements Arrow { + return true; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.AbstractArrow getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.AbstractArrow)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public void setBasePotionData(PotionData data) { + this.setBasePotionType(CraftPotionUtil.fromBukkit(data)); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftAxolotl.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftAxolotl.java +index cbfca242f820d238b112f8ce64e9de8398c48a1c..efbfc8480bddf901fe0acebc06408ee625b57418 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftAxolotl.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftAxolotl.java +@@ -10,8 +10,16 @@ public class CraftAxolotl extends CraftAnimals implements Axolotl, io.papermc.pa + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.axolotl.Axolotl getHandleRaw() { ++ return (net.minecraft.world.entity.animal.axolotl.Axolotl)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.axolotl.Axolotl getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.axolotl.Axolotl) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBat.java +index 1bb72f28085f3885bec068b586ec222111044884..cb56b6690a385e76197cfc0667ebdec72f0cd096 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBat.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBat.java +@@ -8,8 +8,16 @@ public class CraftBat extends CraftAmbient implements Bat { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.ambient.Bat getHandleRaw() { ++ return (net.minecraft.world.entity.ambient.Bat)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.ambient.Bat getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.ambient.Bat) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBee.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBee.java +index 3dac93b0ab5d5acf5b33dc4b0efed60319eb657b..6ade6ca1a32f824271b7deeabc4dd154ae5a67b6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBee.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBee.java +@@ -13,8 +13,16 @@ public class CraftBee extends CraftAnimals implements Bee { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Bee getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Bee)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Bee getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Bee) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlaze.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlaze.java +index a4c9c73691300880777483b0beb17e1bd6779d06..05951297aaed63c22f038703ad6fb68dfcec5227 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlaze.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlaze.java +@@ -8,8 +8,16 @@ public class CraftBlaze extends CraftMonster implements Blaze { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Blaze getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Blaze)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Blaze getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Blaze) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockAttachedEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockAttachedEntity.java +index 5b0dd9aae3fbd9257d0375a37a07c812199d64a2..d22538ecda7685093f400ee560ae53c206ed62b2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockAttachedEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockAttachedEntity.java +@@ -8,8 +8,16 @@ public class CraftBlockAttachedEntity extends CraftEntity { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public BlockAttachedEntity getHandleRaw() { ++ return (BlockAttachedEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public BlockAttachedEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (BlockAttachedEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockDisplay.java +index dd91de8f24c27b9318c2a898a49991d74c100bff..b951571eda47da97ee73ba7d9b71b4f6cf0373d6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockDisplay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockDisplay.java +@@ -12,8 +12,16 @@ public class CraftBlockDisplay extends CraftDisplay implements BlockDisplay { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Display.BlockDisplay getHandleRaw() { ++ return (net.minecraft.world.entity.Display.BlockDisplay)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Display.BlockDisplay getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Display.BlockDisplay) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java +index 2a2839c31989d127739d829159a8b6e5b9a5210b..fb87800c02d5ff9bcb197170c11e305273cea083 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java +@@ -101,8 +101,16 @@ public abstract class CraftBoat extends CraftVehicle implements Boat, io.papermc + return CraftBoat.boatStatusFromNms(this.getHandle().status); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractBoat getHandleRaw() { ++ return (AbstractBoat)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractBoat getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractBoat) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBogged.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBogged.java +index e8e4704304504e69c7964dcd4df8ce5db9e92bf6..20630858d00fa23e911ec38788df971a12f98c6a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBogged.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBogged.java +@@ -12,6 +12,7 @@ public class CraftBogged extends CraftAbstractSkeleton implements Bogged, io.pap + + @Override + public net.minecraft.world.entity.monster.Bogged getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Bogged) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBreeze.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBreeze.java +index 7648e2c700a55f9c0b3539dc720903238d138d54..b21f1654ddd2a4d7c85baae44fef10842905fbf9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBreeze.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBreeze.java +@@ -13,6 +13,7 @@ public class CraftBreeze extends CraftMonster implements Breeze { + + @Override + public net.minecraft.world.entity.monster.breeze.Breeze getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.breeze.Breeze) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftBreezeWindCharge.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftBreezeWindCharge.java +index e88e52a9b8a4d2d750101b0529cbe2a9976e91dd..0eadb421cc505c4639f68c932d284e8ef56f7f57 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftBreezeWindCharge.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftBreezeWindCharge.java +@@ -10,6 +10,7 @@ public class CraftBreezeWindCharge extends CraftAbstractWindCharge implements Br + + @Override + public net.minecraft.world.entity.projectile.windcharge.BreezeWindCharge getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.windcharge.BreezeWindCharge) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCamel.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCamel.java +index 80e571c977db5cdf43bfbfce035f37a3fa325c95..562ac40645f98452d0d923146d4e95c59b029f5b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCamel.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCamel.java +@@ -11,8 +11,16 @@ public class CraftCamel extends CraftAbstractHorse implements Camel { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.camel.Camel getHandleRaw() { ++ return (net.minecraft.world.entity.animal.camel.Camel)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.camel.Camel getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.camel.Camel) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCat.java +index 88e876da7df64b68a5b71fd1deab75b59c5a64e3..3319f46e2c464c553425e33ac31f1d5190c2d1b5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCat.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCat.java +@@ -19,8 +19,16 @@ public class CraftCat extends CraftTameableAnimal implements Cat { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Cat getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Cat)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Cat getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Cat) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCaveSpider.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCaveSpider.java +index 4f661fbdb860cf550da0d952b775fe6f990b43b3..2dfbfbbe98815a303516d88e6ea96b9fba9b7f39 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCaveSpider.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCaveSpider.java +@@ -8,8 +8,16 @@ public class CraftCaveSpider extends CraftSpider implements CaveSpider { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.CaveSpider getHandleRaw() { ++ return (net.minecraft.world.entity.monster.CaveSpider)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.CaveSpider getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.CaveSpider) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java +index a1e04bb965f18ffd07e2f5bf827c5e4ddd6aeeda..8ba8189ddff9f35a60c31015cccf6480246cf21c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java +@@ -15,8 +15,16 @@ public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.ent + this.inventory = new CraftInventory(entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractChestBoat getHandleRaw() { ++ return (AbstractChestBoat)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractChestBoat getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractChestBoat) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestedHorse.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestedHorse.java +index 40ee96e31dea64ab3a77553dbb6daad001736f2e..9cdb7e5ce6883709b709e88037e70a1953d755a0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestedHorse.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestedHorse.java +@@ -10,8 +10,16 @@ public abstract class CraftChestedHorse extends CraftAbstractHorse implements Ch + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractChestedHorse getHandleRaw() { ++ return (AbstractChestedHorse)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractChestedHorse getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractChestedHorse) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChicken.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChicken.java +index 96f6e2fd9c6b20d34122abfe5c7fba732502d5a0..2546ce4d7a25bfe6be1533bfbc770726815e8148 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChicken.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChicken.java +@@ -9,8 +9,16 @@ public class CraftChicken extends CraftAnimals implements Chicken { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Chicken getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Chicken)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Chicken getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Chicken) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCod.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCod.java +index 63e6b07e3b159c74d9ef17be20b5ab43d07f0f5f..44fa01798eed8368fa0187cecb88de830d7d2e16 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCod.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCod.java +@@ -9,8 +9,16 @@ public class CraftCod extends io.papermc.paper.entity.PaperSchoolableFish implem + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Cod getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Cod)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Cod getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Cod) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftComplexPart.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftComplexPart.java +index c2583982d84c736639eec511daba594d7806a628..d31bba789c51bc344d21a357f54dd8ef55b88873 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftComplexPart.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftComplexPart.java +@@ -32,8 +32,16 @@ public class CraftComplexPart extends CraftEntity implements ComplexEntityPart { + return this.getParent().isValid(); + } + ++ // Folia start - region threading ++ @Override ++ public EnderDragonPart getHandleRaw() { ++ return (EnderDragonPart)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public EnderDragonPart getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (EnderDragonPart) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCow.java +index 7babc404e4920cd264206d4e83b1be6f841cdb8c..7a5312ab0fe3a21907a1d6b82fab9b4dce15c44e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCow.java +@@ -9,8 +9,16 @@ public class CraftCow extends CraftAnimals implements Cow { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Cow getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Cow)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Cow getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Cow) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreaking.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreaking.java +index 267f3c85058ef7c73e372c04493cfa6c907e44bb..df838d551fa08895e390eb793506e2f3697555f4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreaking.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreaking.java +@@ -9,8 +9,16 @@ public class CraftCreaking extends CraftMonster implements org.bukkit.entity.Cre + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Creaking getHandleRaw() { ++ return (Creaking)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Creaking getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Creaking) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreature.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreature.java +index 664d9c1793b823ed03f198a936f2ebd9b7695898..6cbe6b6438296b6137ceea01b21ab6a69da2cc9c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreature.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreature.java +@@ -9,8 +9,16 @@ public class CraftCreature extends CraftMob implements Creature { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public PathfinderMob getHandleRaw() { ++ return (PathfinderMob)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public PathfinderMob getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (PathfinderMob) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreeper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreeper.java +index 42dd26b9170f7d217d73f725a6b8440b45ac2190..e59a29ee70e8b1f525c370bb711fa77a5732c500 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftCreeper.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftCreeper.java +@@ -87,6 +87,13 @@ public class CraftCreeper extends CraftMonster implements Creeper { + this.getHandle().ignite(); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Creeper getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Creeper)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Entity getIgniter() { + return (this.getHandle().entityIgniter != null) ? this.getHandle().entityIgniter.getBukkitEntity() : null; +@@ -94,6 +101,7 @@ public class CraftCreeper extends CraftMonster implements Creeper { + + @Override + public net.minecraft.world.entity.monster.Creeper getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Creeper) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftDisplay.java +index 48eeb1d9ba0ad6f895bfe507a6fbe4b9c9530e47..65301b94dc8d813c487deff24cd04b379e666e98 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftDisplay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftDisplay.java +@@ -12,8 +12,16 @@ public class CraftDisplay extends CraftEntity implements Display { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Display getHandleRaw() { ++ return (net.minecraft.world.entity.Display)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Display getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Display) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftDolphin.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftDolphin.java +index 83867b9c5497e6e793b21c482646cc419587e182..55dfb073e4355e68855580f26464af6cf1c6ac33 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftDolphin.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftDolphin.java +@@ -9,8 +9,16 @@ public class CraftDolphin extends CraftAgeable implements Dolphin { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Dolphin getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Dolphin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Dolphin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Dolphin) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftDrowned.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftDrowned.java +index 51fc4acae9f20e8891069704e4a27f212b870766..2b27d3e685ee1882dc6ecc1ceaee2fb52f1b548f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftDrowned.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftDrowned.java +@@ -9,8 +9,16 @@ public class CraftDrowned extends CraftZombie implements Drowned, com.destroysto + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Drowned getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Drowned)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Drowned getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Drowned) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEgg.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEgg.java +index 010e9e922a6e30df4e40da151cfd398d1062633e..8f36a715a5fdf1595cdfdad3d9971cca39279777 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEgg.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEgg.java +@@ -9,8 +9,16 @@ public class CraftEgg extends CraftThrowableProjectile implements Egg { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public ThrownEgg getHandleRaw() { ++ return (ThrownEgg)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ThrownEgg getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ThrownEgg) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderCrystal.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderCrystal.java +index 676dd5331bec75407a74aea2a89e78ab72d69724..4f876511b116dd6e7704f1f047af6fab2c3a3e47 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderCrystal.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderCrystal.java +@@ -39,8 +39,16 @@ public class CraftEnderCrystal extends CraftEntity implements EnderCrystal { + } + } + ++ // Folia start - region threading ++ @Override ++ public EndCrystal getHandleRaw() { ++ return (EndCrystal)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public EndCrystal getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (EndCrystal) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java +index 7b7b89e67d53ed70efae714192c5fa32977f3d9c..747907123b9a9b2b7cae4a20f77455ea48bc04e9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java +@@ -30,8 +30,16 @@ public class CraftEnderDragon extends CraftMob implements EnderDragon, CraftEnem + return builder.build(); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.boss.enderdragon.EnderDragon getHandleRaw() { ++ return (net.minecraft.world.entity.boss.enderdragon.EnderDragon)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.boss.enderdragon.EnderDragon getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.boss.enderdragon.EnderDragon) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragonPart.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragonPart.java +index 33ae03b78b01c005a291a343b42507fb539e81a6..36aec95539044edd429c17833338638262b9db00 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragonPart.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragonPart.java +@@ -16,8 +16,16 @@ public class CraftEnderDragonPart extends CraftComplexPart implements EnderDrago + return (EnderDragon) super.getParent(); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.boss.EnderDragonPart getHandleRaw() { ++ return (net.minecraft.world.entity.boss.EnderDragonPart)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.boss.EnderDragonPart getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.boss.EnderDragonPart) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderPearl.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderPearl.java +index 3bb8d74f2b59c7f0c7c1cbde47a570d628ceceb2..25d7577d17d52dc00a355a684f1493efb2e88584 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderPearl.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderPearl.java +@@ -9,8 +9,16 @@ public class CraftEnderPearl extends CraftThrowableProjectile implements EnderPe + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public ThrownEnderpearl getHandleRaw() { ++ return (ThrownEnderpearl)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ThrownEnderpearl getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ThrownEnderpearl) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderSignal.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderSignal.java +index 27f56fa4b7ef92a9a4dfa6b782350424b88210f2..e76390fe22e2e846313c9a5b2c7f5492f798ca3e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderSignal.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderSignal.java +@@ -15,8 +15,16 @@ public class CraftEnderSignal extends CraftEntity implements EnderSignal { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public EyeOfEnder getHandleRaw() { ++ return (EyeOfEnder)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public EyeOfEnder getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (EyeOfEnder) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderman.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderman.java +index 983b9d6ddb58eff297e96e5c8b28ec427efa267d..16e33e302f8a60f1f9ff67929dc7c63cd5192a37 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderman.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderman.java +@@ -62,8 +62,16 @@ public class CraftEnderman extends CraftMonster implements Enderman { + } + // Paper end + ++ // Folia start - region threading ++ @Override ++ public EnderMan getHandleRaw() { ++ return (EnderMan)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public EnderMan getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (EnderMan) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +index d657fd2c507a5b215aeab0a5f3e9c2ee892a27c8..399ef60ab5f1bf02b638c8c46a72d297932f6b38 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java +@@ -9,8 +9,16 @@ public class CraftEndermite extends CraftMonster implements Endermite { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Endermite getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Endermite)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Endermite getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Endermite) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 5168cf0d58013aecfd80d37fb698014f38f8e08d..982778c4828e79bc7a55745418beb04f9c56cc78 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -514,6 +514,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + public Entity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEvoker.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEvoker.java +index 3a890cccf1766758794f3a3b5d31428f42590049..8c148db1b84c65b89fb2779e5b96a71ea4900083 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEvoker.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEvoker.java +@@ -11,8 +11,16 @@ public class CraftEvoker extends CraftSpellcaster implements Evoker { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Evoker getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Evoker)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Evoker getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Evoker) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEvokerFangs.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEvokerFangs.java +index 19b368cc862cd7e3e1f0e89401a7d099e3eaefa3..4a1c1af06719ff75f6ec2ac27198858b549b0302 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEvokerFangs.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEvokerFangs.java +@@ -11,8 +11,16 @@ public class CraftEvokerFangs extends CraftEntity implements EvokerFangs { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.EvokerFangs getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.EvokerFangs)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.EvokerFangs getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.EvokerFangs) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftExperienceOrb.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftExperienceOrb.java +index 650e4a01cecc4cc08e7ff9ebcc4c367084351f21..81b2b850dd7d08f2fae7baf56733d753b68d294c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftExperienceOrb.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftExperienceOrb.java +@@ -42,8 +42,16 @@ public class CraftExperienceOrb extends CraftEntity implements ExperienceOrb { + } + // Paper end + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.ExperienceOrb getHandleRaw() { ++ return (net.minecraft.world.entity.ExperienceOrb)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.ExperienceOrb getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.ExperienceOrb) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java +index 1359d25a32b4a5d5e8e68ce737bd19f7b5afaf69..4c6ac7f2531311d24081b397c60b2f8b183fad34 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java +@@ -14,8 +14,16 @@ public class CraftFallingBlock extends CraftEntity implements FallingBlock { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public FallingBlockEntity getHandleRaw() { ++ return (FallingBlockEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public FallingBlockEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (FallingBlockEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java +index 43d7bea201a52cfeacf60c75caa28dfd2c4ff164..ac7237e8c28377d5f9abf38b628215ac865c9709 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java +@@ -83,8 +83,16 @@ public class CraftFireball extends AbstractProjectile implements Fireball { + } + // Paper end - Expose power on fireball projectiles + ++ // Folia start - region threading ++ @Override ++ public AbstractHurtingProjectile getHandleRaw() { ++ return (AbstractHurtingProjectile)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractHurtingProjectile getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractHurtingProjectile) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java +index 759b6e54db93792c9862b1f1625118ac6fa49d7a..6fdd39c78a2f7c1c53d5de16e09e0f271c42039e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java +@@ -37,8 +37,16 @@ public class CraftFirework extends CraftProjectile implements Firework { + // Paper end - Expose firework item directly + } + ++ // Folia start - region threading ++ @Override ++ public FireworkRocketEntity getHandleRaw() { ++ return (FireworkRocketEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public FireworkRocketEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (FireworkRocketEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFish.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFish.java +index eb10f94d5ed8ca89d3786138647dd43357609a6c..f4d92fb44fd7cee7debe3e283e8b672021e3e23f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFish.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFish.java +@@ -10,8 +10,16 @@ public class CraftFish extends CraftWaterMob implements Fish, io.papermc.paper.e + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractFish getHandleRaw() { ++ return (AbstractFish)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractFish getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractFish) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java +index e0d65df2e5b4c14abeb89a5f72cc2d9fa034dcf5..bd8f1925cb3eee30a5b5ea83225b6d94c80bc69a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java +@@ -14,8 +14,16 @@ public class CraftFishHook extends CraftProjectile implements FishHook { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public FishingHook getHandleRaw() { ++ return (FishingHook)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public FishingHook getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (FishingHook) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFlying.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFlying.java +index 8117faa0c89a966d057f4bf251c03a09d1e8797e..7c3827e6ef608ff15be9bced4788b09f1572aecb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFlying.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFlying.java +@@ -10,8 +10,16 @@ public class CraftFlying extends CraftMob implements Flying { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public FlyingMob getHandleRaw() { ++ return (FlyingMob)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public FlyingMob getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (FlyingMob) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFox.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFox.java +index bb2b59ce9775a0d1dd9828885e57c14cf40d9f04..90dcbf746c5effa98c09059552674a3e428ac1b9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFox.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFox.java +@@ -14,8 +14,16 @@ public class CraftFox extends CraftAnimals implements Fox { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Fox getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Fox)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Fox getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Fox) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftFrog.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftFrog.java +index ebf09c27e02a19d31c777b70a38376e4d01e5ee7..0eb55d6e7541acbc6727a108fdeed1711a17f3cd 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftFrog.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftFrog.java +@@ -19,8 +19,16 @@ public class CraftFrog extends CraftAnimals implements org.bukkit.entity.Frog { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Frog getHandleRaw() { ++ return (Frog)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Frog getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Frog) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGhast.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGhast.java +index 97fa4e1e70203194bd939618b2fad92665af6d59..27b309c9ce10798e3c3a7a9d39b8c300e471e177 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGhast.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGhast.java +@@ -9,8 +9,16 @@ public class CraftGhast extends CraftFlying implements Ghast, CraftEnemy { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Ghast getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Ghast)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Ghast getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Ghast) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGiant.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGiant.java +index 5826205339e99e2536b93c8589d95917749f8417..9bb22fc146012310bca849fccb0a1e7e987875e9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGiant.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGiant.java +@@ -9,8 +9,16 @@ public class CraftGiant extends CraftMonster implements Giant { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Giant getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Giant)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Giant getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Giant) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowItemFrame.java +index b9a7576d2481b64b7e5b46d66c1f55d1dc28c540..00c95313a233a032518e2435922d4044a9d67aee 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowItemFrame.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowItemFrame.java +@@ -9,8 +9,16 @@ public class CraftGlowItemFrame extends CraftItemFrame implements GlowItemFrame + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.decoration.GlowItemFrame getHandleRaw() { ++ return (net.minecraft.world.entity.decoration.GlowItemFrame)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.decoration.GlowItemFrame getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.decoration.GlowItemFrame) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowSquid.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowSquid.java +index 253a0d2f987163cbbb28d261674b47137cbbcbe2..1ed09d2aa4077165e9f88dd9db34f4083a2953c2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowSquid.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowSquid.java +@@ -10,8 +10,16 @@ public class CraftGlowSquid extends CraftSquid implements GlowSquid { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.GlowSquid getHandleRaw() { ++ return (net.minecraft.world.entity.GlowSquid)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.GlowSquid getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.GlowSquid) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGoat.java +index 2c21de478bff9cdf13ba46cd041831d54c11e924..e64d7c4cfe65d34bdab13496741645f808f43dc6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGoat.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGoat.java +@@ -9,8 +9,16 @@ public class CraftGoat extends CraftAnimals implements Goat { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.goat.Goat getHandleRaw() { ++ return (net.minecraft.world.entity.animal.goat.Goat)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.goat.Goat getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.goat.Goat) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGolem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGolem.java +index e27e469894bdd17cf7a004a85fdf0eaa746111a6..bbb8ff66580e62b5fb66aac22de72b9b9eafd3ef 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGolem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGolem.java +@@ -9,8 +9,16 @@ public class CraftGolem extends CraftCreature implements Golem { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractGolem getHandleRaw() { ++ return (AbstractGolem)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractGolem getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractGolem) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftGuardian.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftGuardian.java +index e232350f2c6ef1900b05fda4d3f94099057d10e5..2c411b569cc4b222ed3cdfb95237c86cd6a0fabb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftGuardian.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftGuardian.java +@@ -13,8 +13,16 @@ public class CraftGuardian extends CraftMonster implements Guardian { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Guardian getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Guardian)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Guardian getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Guardian) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHanging.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHanging.java +index f1e3f2b89bcd969f3c80548e165881a9b290eb53..2e4b86b44ace5eecefc9ab09c6e1f0a31247ad2f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHanging.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHanging.java +@@ -57,8 +57,16 @@ public class CraftHanging extends CraftBlockAttachedEntity implements Hanging { + return CraftBlock.notchToBlockFace(direction); + } + ++ // Folia start - region threading ++ @Override ++ public HangingEntity getHandleRaw() { ++ return (HangingEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public HangingEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (HangingEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHoglin.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHoglin.java +index 37007775d27598e319c0c78929c6a808b697724a..b9819fc2c2ffc1a21a6e0973bb0d3595ee9c565d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHoglin.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHoglin.java +@@ -51,8 +51,16 @@ public class CraftHoglin extends CraftAnimals implements Hoglin, CraftEnemy { + return this.getHandle().isConverting(); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.hoglin.Hoglin getHandleRaw() { ++ return (net.minecraft.world.entity.monster.hoglin.Hoglin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.hoglin.Hoglin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.hoglin.Hoglin) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHorse.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHorse.java +index 9b6ff0f64966c78a3233860bb0840182b52f01bc..fb34651a9e4ed0cb05721d15524a26f89333d5e7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHorse.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHorse.java +@@ -13,8 +13,16 @@ public class CraftHorse extends CraftAbstractHorse implements Horse { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.horse.Horse getHandleRaw() { ++ return (net.minecraft.world.entity.animal.horse.Horse)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.horse.Horse getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.horse.Horse) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +index e345cdbfab44a0f5da80d738798dbb4424b7ab5c..2a4ed66335e4fd88aefabb063ec04fe803bc728e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +@@ -298,8 +298,16 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { + this.mode = mode; + } + ++ // Folia start - region threading ++ @Override ++ public Player getHandleRaw() { ++ return (Player)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Player getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Player) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIllager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIllager.java +index fb3c518f02cb4c428f022523d2f838625841332b..846a429493236f5002f0fae85c6cd7d20169dbe0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIllager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIllager.java +@@ -10,8 +10,16 @@ public class CraftIllager extends CraftRaider implements Illager { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractIllager getHandleRaw() { ++ return (AbstractIllager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractIllager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractIllager) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIllusioner.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIllusioner.java +index 5b2af80e584977683cd39e6f440e65a76e929be9..789191168f74b3272e8da2131e0311853033c938 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIllusioner.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIllusioner.java +@@ -9,8 +9,16 @@ public class CraftIllusioner extends CraftSpellcaster implements Illusioner, com + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Illusioner getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Illusioner)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Illusioner getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Illusioner) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftInteraction.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftInteraction.java +index caa3016bf9742222205e3ea9a327fad3c4f912bb..2e00c7fe8dadd4c57c83a51cdfce165b6bfd6807 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftInteraction.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftInteraction.java +@@ -12,8 +12,16 @@ public class CraftInteraction extends CraftEntity implements Interaction { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Interaction getHandleRaw() { ++ return (net.minecraft.world.entity.Interaction)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Interaction getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Interaction) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +index 63cae1a2e95d8da17c45c4404a8dd0ca6a413c39..e417ff87b047dcffa6121835af6f4e713526e16b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java +@@ -8,8 +8,16 @@ public class CraftIronGolem extends CraftGolem implements IronGolem { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.IronGolem getHandleRaw() { ++ return (net.minecraft.world.entity.animal.IronGolem)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.IronGolem getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.IronGolem) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +index 30d62ee4d5cd2ddacb8783b5bbbf475d592b3e02..3985b6bea750341f5336babb237aab8874a4cbd9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java +@@ -18,8 +18,16 @@ public class CraftItem extends CraftEntity implements Item { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public ItemEntity getHandleRaw() { ++ return (ItemEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ItemEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ItemEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemDisplay.java +index 787f91566fc53c2b4aeba1ec10d8f46ccf15cbe6..04a73a31ba09557e901ff1985dc5d5e53f18d99a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemDisplay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemDisplay.java +@@ -13,8 +13,16 @@ public class CraftItemDisplay extends CraftDisplay implements ItemDisplay { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Display.ItemDisplay getHandleRaw() { ++ return (net.minecraft.world.entity.Display.ItemDisplay)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Display.ItemDisplay getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Display.ItemDisplay) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +index 350ad61ab3fe66abd528e353b431a4a6dac17506..332f209980d3e645ad469fcebb93cc09253ebc20 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +@@ -157,8 +157,16 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame { + this.getHandle().fixed = fixed; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.decoration.ItemFrame getHandleRaw() { ++ return (net.minecraft.world.entity.decoration.ItemFrame)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.decoration.ItemFrame getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.decoration.ItemFrame) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLargeFireball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLargeFireball.java +index 0848963e61e03aa2a1740208ee372fd9edb7fc11..de2236f0106330ebe9d76bd308f9eee8751db826 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLargeFireball.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLargeFireball.java +@@ -14,8 +14,16 @@ public class CraftLargeFireball extends CraftSizedFireball implements LargeFireb + this.getHandle().explosionPower = (int) yield; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.LargeFireball getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.LargeFireball)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.LargeFireball getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.LargeFireball) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLeash.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLeash.java +index 76a7fc3d6c561d12bde17b9f93cae03a6cbb84b3..cd1ba99a75da644d06c4eb2f2c1ff91bfa5afa01 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLeash.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLeash.java +@@ -24,6 +24,13 @@ public class CraftLeash extends CraftBlockAttachedEntity implements LeashHitch { + return BlockFace.SELF; + } + ++ // Folia start - region threading ++ @Override ++ public LeashFenceKnotEntity getHandleRaw() { ++ return (LeashFenceKnotEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public BlockFace getAttachedFace() { + // Leash hitch has no facing direction, so we return self +@@ -37,6 +44,7 @@ public class CraftLeash extends CraftBlockAttachedEntity implements LeashHitch { + + @Override + public LeashFenceKnotEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (LeashFenceKnotEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java +index e9f471e60af0725ec34e2985d63ae9ea9f88590a..cd824fc65ac2b1fe55710da4700f7c31f820f205 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java +@@ -41,8 +41,16 @@ public class CraftLightningStrike extends CraftEntity implements LightningStrike + this.getHandle().setCause((player != null) ? ((CraftPlayer) player).getHandle() : null); + } + ++ // Folia start - region threading ++ @Override ++ public LightningBolt getHandleRaw() { ++ return (LightningBolt)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public LightningBolt getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (LightningBolt) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +index 4f98d138a275a6c34528b7a5148ef265bc38d6b5..228f7fbce72b7828905e21f21525371a92ec07d4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +@@ -487,6 +487,13 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + this.getHandle().invulnerableTime = ticks; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.LivingEntity getHandleRaw() { ++ return (net.minecraft.world.entity.LivingEntity)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public int getNoActionTicks() { + return this.getHandle().getNoActionTime(); +@@ -500,6 +507,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity { + + @Override + public net.minecraft.world.entity.LivingEntity getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.LivingEntity) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +index 351f42842b780d053cd2e5bad9ae299449141b10..63513eff9b849f240b16ea28060b78c774e23934 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java +@@ -14,8 +14,16 @@ public class CraftLlama extends CraftChestedHorse implements Llama, com.destroys + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.horse.Llama getHandleRaw() { ++ return (net.minecraft.world.entity.animal.horse.Llama)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.horse.Llama getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.horse.Llama) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java +index 47633f05b4fab1dcabc2117e7645fe6d6949622a..5e51d6eeda2abdc5df9c9a280a191ca1cbf615b9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java +@@ -10,8 +10,16 @@ public class CraftLlamaSpit extends AbstractProjectile implements LlamaSpit { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.LlamaSpit getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.LlamaSpit)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.LlamaSpit getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.LlamaSpit) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMagmaCube.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMagmaCube.java +index 58b638ffd338e1b0f4962490c665c1eebcf33dcc..9f1b4d0561c10fbbfe0daec3d9dabfdaca9cf70b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMagmaCube.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMagmaCube.java +@@ -9,8 +9,16 @@ public class CraftMagmaCube extends CraftSlime implements MagmaCube { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.MagmaCube getHandleRaw() { ++ return (net.minecraft.world.entity.monster.MagmaCube)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.MagmaCube getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.MagmaCube) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMarker.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMarker.java +index e6782a48d22ba1e683e3fe463e970e8a5ed60fbd..afaa4570c1991cd4260ffcdba823ba2452ad156a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMarker.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMarker.java +@@ -9,8 +9,16 @@ public class CraftMarker extends CraftEntity implements Marker { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Marker getHandleRaw() { ++ return (net.minecraft.world.entity.Marker)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Marker getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Marker) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecart.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecart.java +index b42bce0c4f4b3aac2729cfdad392d863245ed693..d3ffa2b4402fdd005104d07d92e4066c6170615e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecart.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecart.java +@@ -77,8 +77,16 @@ public abstract class CraftMinecart extends CraftVehicle implements Minecart { + } + // Paper end + ++ // Folia start - region threading ++ @Override ++ public AbstractMinecart getHandleRaw() { ++ return (AbstractMinecart)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractMinecart getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractMinecart) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java +index f34fa6715e477936097367a7aefd1a2bf87d3d90..e5310b138b13d54448072c15f6768acc1c33a45c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java +@@ -20,8 +20,16 @@ public class CraftMinecartCommand extends CraftMinecart implements CommandMineca + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public MinecartCommandBlock getHandleRaw() { ++ return (MinecartCommandBlock)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public MinecartCommandBlock getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (MinecartCommandBlock) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java +index 451f3a6f0b47493da3af3f5d6baced6a8c97f350..d4f98fe5eb5e463679ebc5b82b077c98e4448203 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java +@@ -13,8 +13,16 @@ public abstract class CraftMinecartContainer extends CraftMinecart implements co + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public AbstractMinecartContainer getHandleRaw() { ++ return (AbstractMinecartContainer)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractMinecartContainer getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractMinecartContainer) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartFurnace.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartFurnace.java +index 1be1f6d23f2224d4d8720d40f2e530736b1bae81..eee08d53714b485bffd1398506ed0cb3b7002d2c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartFurnace.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartFurnace.java +@@ -11,8 +11,16 @@ public class CraftMinecartFurnace extends CraftMinecart implements PoweredMineca + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public MinecartFurnace getHandleRaw() { ++ return (MinecartFurnace)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public MinecartFurnace getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (MinecartFurnace) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java +index 3a3563a1bdbc0d84d973b3a04b50b78b4bc3d379..1e86ce7c1a3fc1f4eae2d8136fc0d879fbde5301 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java +@@ -34,8 +34,17 @@ public final class CraftMinecartHopper extends CraftMinecartContainer implements + ((MinecartHopper) this.getHandle()).setEnabled(enabled); + } + // Paper start ++ ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.vehicle.MinecartHopper getHandleRaw() { ++ return (net.minecraft.world.entity.vehicle.MinecartHopper)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.vehicle.MinecartHopper getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.vehicle.MinecartHopper) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartMobSpawner.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartMobSpawner.java +index e8ece01669373ecf6552d33b2ed72668524e2650..fbb5c2e2a136cd03eb1f4b4b5ef289d6a6c39173 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartMobSpawner.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartMobSpawner.java +@@ -162,8 +162,16 @@ final class CraftMinecartMobSpawner extends CraftMinecart implements SpawnerMine + this.getHandle().getSpawner().spawnRange = spawnRange; + } + ++ // Folia start - region threading ++ @Override ++ public MinecartSpawner getHandleRaw() { ++ return (MinecartSpawner)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public MinecartSpawner getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (MinecartSpawner) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartTNT.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartTNT.java +index 15184e7fc3aeb388fb9de6be2ad72f98fee52044..f18093c5ccacfb55e7c6133cf5212c464e41ead4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartTNT.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartTNT.java +@@ -72,8 +72,16 @@ public final class CraftMinecartTNT extends CraftMinecart implements ExplosiveMi + this.getHandle().explode(power); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.vehicle.MinecartTNT getHandleRaw() { ++ return (net.minecraft.world.entity.vehicle.MinecartTNT)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public MinecartTNT getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (MinecartTNT) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMob.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMob.java +index 778a9d3f8bfe5dba59e1e655e4eeb8822678b8cf..b4ec6c1f8ea5d5c34f2ecb2b066e49993ae79dc7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMob.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMob.java +@@ -54,8 +54,16 @@ public abstract class CraftMob extends CraftLivingEntity implements Mob, io.pape + return (sound != null) ? CraftSound.minecraftToBukkit(sound) : null; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Mob getHandleRaw() { ++ return (net.minecraft.world.entity.Mob)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Mob getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Mob) this.entity; + } + +@@ -63,7 +71,7 @@ public abstract class CraftMob extends CraftLivingEntity implements Mob, io.pape + @Override + public void setHandle(net.minecraft.world.entity.Entity entity) { + super.setHandle(entity); +- paperPathfinder.setHandle(getHandle()); ++ paperPathfinder.setHandle((net.minecraft.world.entity.Mob)entity); // Folia - region threading + } + // Paper end - Mob Pathfinding API + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMonster.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMonster.java +index 706c74c832f6893df3797023f68add31139c7d57..1cf155fc23f13691f86673eac3084d7530d69ab5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMonster.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMonster.java +@@ -9,8 +9,16 @@ public class CraftMonster extends CraftCreature implements Monster, CraftEnemy { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Monster getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Monster)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Monster getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Monster) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java +index 596146ad7899c21645df8834ce5f0afd6c1b0604..78f6e16a745924419d5aad53f95d767d87bdf5d0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java +@@ -19,6 +19,13 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.MushroomCow getHandleRaw() { ++ return (net.minecraft.world.entity.animal.MushroomCow)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public boolean hasEffectsForNextStew() { + SuspiciousStewEffects stewEffects = this.getHandle().stewEffects; +@@ -94,6 +101,7 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm + + @Override + public net.minecraft.world.entity.animal.MushroomCow getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.MushroomCow) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftOcelot.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftOcelot.java +index 5c60a30e80448fbf04b5fa4b1ef12fb2ee99bfd5..4ba52939450c0a89e5ba1fa57a84b3ceccb9fef0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftOcelot.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftOcelot.java +@@ -9,8 +9,16 @@ public class CraftOcelot extends CraftAnimals implements Ocelot { + super(server, ocelot); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Ocelot getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Ocelot)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Ocelot getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Ocelot) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftOminousItemSpawner.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftOminousItemSpawner.java +index ecdac2cf74e99f0d69e053dece11ab891973dc2b..fa365c38c9e0f671df1481c8b36bc993eee42afd 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftOminousItemSpawner.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftOminousItemSpawner.java +@@ -13,6 +13,7 @@ public class CraftOminousItemSpawner extends CraftEntity implements OminousItemS + + @Override + public net.minecraft.world.entity.OminousItemSpawner getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.OminousItemSpawner) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPainting.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPainting.java +index b1b139b773b37e6ec2afea85c500387d6ba9800e..38c1eb97de420cd7dea6a9f76ef644ecdf8c30b8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPainting.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPainting.java +@@ -50,8 +50,16 @@ public class CraftPainting extends CraftHanging implements Painting { + return false; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.decoration.Painting getHandleRaw() { ++ return (net.minecraft.world.entity.decoration.Painting)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.decoration.Painting getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.decoration.Painting) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPanda.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPanda.java +index 01d104d91de9e1319d27e39d3f474318c7809486..c298b263175dc82097c0ad2c35194f3e326c6658 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPanda.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPanda.java +@@ -11,8 +11,16 @@ public class CraftPanda extends CraftAnimals implements Panda { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Panda getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Panda)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Panda getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Panda) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftParrot.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftParrot.java +index 04d6cf6a1f3ae8316e3b2862c2d1b04e84a3b20a..4ed79610b50be635a7a7c8a8f7d7af8f91ce2d0d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftParrot.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftParrot.java +@@ -11,8 +11,16 @@ public class CraftParrot extends CraftTameableAnimal implements Parrot { + super(server, parrot); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Parrot getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Parrot)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Parrot getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Parrot) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java +index 83e77c6d287d8e239d2f55f3e9f19ef74946be7c..10e385066d29834eb3d8c9d539bb8655407cabb5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java +@@ -9,8 +9,16 @@ public class CraftPhantom extends CraftFlying implements Phantom, CraftEnemy { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Phantom getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Phantom)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Phantom getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Phantom) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java +index 8016c810aeaf6ee953bca549bc1e7f9a85f860fc..e96e58fa4fb2a73e3e44c5213c73f332df4daa97 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java +@@ -55,8 +55,16 @@ public class CraftPig extends CraftAnimals implements Pig { + return Material.CARROT_ON_A_STICK; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Pig getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Pig)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Pig getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Pig) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPigZombie.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPigZombie.java +index 49beb836d2801aadf869feefa602616daebe633f..d220874f678649acfae549691262c370f0228908 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPigZombie.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPigZombie.java +@@ -30,8 +30,16 @@ public class CraftPigZombie extends CraftZombie implements PigZombie { + return this.getAnger() > 0; + } + ++ // Folia start - region threading ++ @Override ++ public ZombifiedPiglin getHandleRaw() { ++ return (ZombifiedPiglin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ZombifiedPiglin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ZombifiedPiglin) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglin.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglin.java +index 5124a383b60b2c8de89fa992547d0c61db760c21..d75230de45102434660b3b7926a804d26e10ab2c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglin.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglin.java +@@ -75,8 +75,16 @@ public class CraftPiglin extends CraftPiglinAbstract implements Piglin, com.dest + return new CraftInventory(this.getHandle().inventory); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.piglin.Piglin getHandleRaw() { ++ return (net.minecraft.world.entity.monster.piglin.Piglin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.piglin.Piglin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.piglin.Piglin) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinAbstract.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinAbstract.java +index e7957d6051244ba410f8633f9c16eeb8c5ac3ce0..f8465f75c15d96ccd82ee394c9e658966837ad07 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinAbstract.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinAbstract.java +@@ -95,8 +95,16 @@ public class CraftPiglinAbstract extends CraftMonster implements PiglinAbstract + public void setBreed(boolean b) { + } + ++ // Folia start - region threading ++ @Override ++ public AbstractPiglin getHandleRaw() { ++ return (AbstractPiglin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public AbstractPiglin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (AbstractPiglin) super.getHandle(); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinBrute.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinBrute.java +index be874dc973fe632e8ace86041392ca69beaefd16..efb64160089eeb6be8faf7790989909145c22a4b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinBrute.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinBrute.java +@@ -9,8 +9,16 @@ public class CraftPiglinBrute extends CraftPiglinAbstract implements PiglinBrute + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.piglin.PiglinBrute getHandleRaw() { ++ return (net.minecraft.world.entity.monster.piglin.PiglinBrute)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.piglin.PiglinBrute getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.piglin.PiglinBrute) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPillager.java +index 2638c341bc02f201f7ab17fdebcdbdf3a7ec05bf..074b2919be2b5544b0a46e6cd32f6c57dad6bfdc 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPillager.java +@@ -11,8 +11,16 @@ public class CraftPillager extends CraftIllager implements Pillager, com.destroy + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Pillager getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Pillager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Pillager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Pillager) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index f2a847e590c72eee91a053cecdc691c53751ca3a..b0e93050839ce00b057e3a9bf3bdf8dd5e0662cf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -683,7 +683,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void kick(net.kyori.adventure.text.Component message, org.bukkit.event.player.PlayerKickEvent.Cause cause) { +- org.spigotmc.AsyncCatcher.catchOp("player kick"); ++ //org.spigotmc.AsyncCatcher.catchOp("player kick"); // Folia - region threading - no longer needed + final ServerGamePacketListenerImpl connection = this.getHandle().connection; + if (connection != null) { + connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message, cause); +@@ -2336,9 +2336,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return this; + } + ++ // Folia start - region threading ++ @Override ++ public ServerPlayer getHandleRaw() { ++ return (ServerPlayer)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ServerPlayer getHandle() { +- return (ServerPlayer) this.entity; ++ return (ServerPlayer) this.entity; // Folia - region threading - no checks for players, as it's a total mess + } + + public void setHandle(final ServerPlayer entity) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPolarBear.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPolarBear.java +index fe075cfdf3097d6cb768e71b8cc360abb8eaf367..657886dfb8e152ed4a64a64878da23526dad0160 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPolarBear.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPolarBear.java +@@ -8,8 +8,17 @@ public class CraftPolarBear extends CraftAnimals implements PolarBear { + public CraftPolarBear(CraftServer server, net.minecraft.world.entity.animal.PolarBear entity) { + super(server, entity); + } ++ ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.PolarBear getHandleRaw() { ++ return (net.minecraft.world.entity.animal.PolarBear)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.PolarBear getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.PolarBear) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java +index 4f1fa7dec78970bdfc184d3c1f1632dc9d75a574..99fd39c60d1b0a50bddf7b9b9f45f22c189a2f25 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java +@@ -12,8 +12,16 @@ public abstract class CraftProjectile extends AbstractProjectile implements Proj + + // Paper - moved to AbstractProjectile + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.Projectile getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.Projectile)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.Projectile getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.Projectile) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPufferFish.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPufferFish.java +index 35a8219734633529325430810e88755b2dd23125..7ba16121cb1828cf5c0ff8f027fa05e9c1814ffa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPufferFish.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPufferFish.java +@@ -10,8 +10,16 @@ public class CraftPufferFish extends CraftFish implements PufferFish { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Pufferfish getHandleRaw() { ++ return (Pufferfish)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Pufferfish getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Pufferfish) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftRabbit.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftRabbit.java +index 519ef701a7d6534f7cb516f6296b95ee521f661d..6407b4e6ca793a676e7d669920ae90b762207970 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftRabbit.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftRabbit.java +@@ -10,8 +10,16 @@ public class CraftRabbit extends CraftAnimals implements Rabbit { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Rabbit getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Rabbit)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Rabbit getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Rabbit) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftRaider.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftRaider.java +index 763c368e299588f9a0e085a8a5e04e97e1f33428..3e85638f3941c2085a7ddb102d0ccc23446cc1d6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftRaider.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftRaider.java +@@ -16,8 +16,16 @@ public abstract class CraftRaider extends CraftMonster implements Raider { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.raid.Raider getHandleRaw() { ++ return (net.minecraft.world.entity.raid.Raider)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.raid.Raider getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.raid.Raider) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftRavager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftRavager.java +index 09796ce15658e3f7c223a265a547a51ee729ed40..bfca2951d18f7451787877b5a6503b0572945447 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftRavager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftRavager.java +@@ -9,8 +9,16 @@ public class CraftRavager extends CraftRaider implements Ravager { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Ravager getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Ravager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Ravager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Ravager) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSalmon.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSalmon.java +index 7660cc21e936002ebb23510f0ec2b58d71e5157d..a13976b2712413ef9fdeecd1e3ca762238d4efd9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSalmon.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSalmon.java +@@ -10,8 +10,16 @@ public class CraftSalmon extends io.papermc.paper.entity.PaperSchoolableFish imp + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Salmon getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Salmon)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Salmon getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Salmon) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSheep.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSheep.java +index 37291d7ad9fdf0fe78894f82a418f40bb581f58b..6c7e54a929b46fd160726e41bf63023a8622d044 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSheep.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSheep.java +@@ -29,8 +29,16 @@ public class CraftSheep extends CraftAnimals implements Sheep, io.papermc.paper. + this.getHandle().setSheared(flag); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Sheep getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Sheep)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Sheep getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Sheep) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulker.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulker.java +index 05ec06b71642ab1ef03829039f7ac1e4c527ee50..1e1e908cbc08df06996128e3dd6d277a19f9a2df 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulker.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulker.java +@@ -18,8 +18,16 @@ public class CraftShulker extends CraftGolem implements Shulker, CraftEnemy { + return "CraftShulker"; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Shulker getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Shulker)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Shulker getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Shulker) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java +index b3797a43eeee11cb7ae0774d61bd5f195d0aa3ad..d045d50d1cfccb696153b8c33e86e193194271fc 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java +@@ -69,8 +69,16 @@ public class CraftShulkerBullet extends AbstractProjectile implements ShulkerBul + return "CraftShulkerBullet"; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.ShulkerBullet getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.ShulkerBullet)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.ShulkerBullet getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.ShulkerBullet) this.entity; + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSilverfish.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSilverfish.java +index 7c75d78e5e28d7320c6dbe979bcd576658fb310b..a25ca7fa49a3bb213f6af5804079b2efe43ef0e4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSilverfish.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSilverfish.java +@@ -8,8 +8,16 @@ public class CraftSilverfish extends CraftMonster implements Silverfish { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Silverfish getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Silverfish)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Silverfish getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Silverfish) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSizedFireball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSizedFireball.java +index de3327812c08b3bb8f5907ae657f67962d1e4e8b..c479f4adb945e8bb6ea2279ad23d679ca0dee606 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSizedFireball.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSizedFireball.java +@@ -27,8 +27,16 @@ public class CraftSizedFireball extends CraftFireball implements SizedFireball { + this.getHandle().setItem(CraftItemStack.asNMSCopy(item)); + } + ++ // Folia start - region threading ++ @Override ++ public Fireball getHandleRaw() { ++ return (Fireball)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Fireball getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Fireball) this.entity; + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeleton.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeleton.java +index 6f98da9be6aef35e3b5c940188b872459a383c8e..dc93b8aaf48671d66d3bb3fb413b83fc4b4b26cf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeleton.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeleton.java +@@ -31,8 +31,16 @@ public class CraftSkeleton extends CraftAbstractSkeleton implements Skeleton { + } + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Skeleton getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Skeleton)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Skeleton getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Skeleton) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeletonHorse.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeletonHorse.java +index fbb47491dcc75f8247dee9f123f946f99ef1467f..6cc1ea31340298037c2a00d64d70928f31278a4a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeletonHorse.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeletonHorse.java +@@ -20,8 +20,16 @@ public class CraftSkeletonHorse extends CraftAbstractHorse implements SkeletonHo + return Variant.SKELETON_HORSE; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.horse.SkeletonHorse getHandleRaw() { ++ return (net.minecraft.world.entity.animal.horse.SkeletonHorse)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.horse.SkeletonHorse getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.horse.SkeletonHorse) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSlime.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSlime.java +index e48f7d1cbec4a2319745ba48a5d44ab9925214e2..27b07865edfa659d9cdfcf2d84935ad313472e87 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSlime.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSlime.java +@@ -19,8 +19,16 @@ public class CraftSlime extends CraftMob implements Slime, CraftEnemy { + this.getHandle().setSize(size, /* true */ getHandle().isAlive()); // Paper - fix dead slime setSize invincibility + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Slime getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Slime)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Slime getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Slime) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSmallFireball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSmallFireball.java +index 072df206858944ef78179b0a6d61ed990a844d2b..71625cc4e4b2fd3773baf1b2c1ea7e463b854ffa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSmallFireball.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSmallFireball.java +@@ -8,8 +8,16 @@ public class CraftSmallFireball extends CraftSizedFireball implements SmallFireb + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.SmallFireball getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.SmallFireball)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.SmallFireball getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.SmallFireball) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSniffer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSniffer.java +index 555337018fe218ac5a296a5e6a1d82720fee05e1..873b7e7a05b3465b79a82ed583ce16bb245ebcbf 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSniffer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSniffer.java +@@ -16,8 +16,16 @@ public class CraftSniffer extends CraftAnimals implements Sniffer { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.sniffer.Sniffer getHandleRaw() { ++ return (net.minecraft.world.entity.animal.sniffer.Sniffer)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.sniffer.Sniffer getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.sniffer.Sniffer) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowball.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowball.java +index d959825fd11a94aba175934cd7739544a23958fc..9f53ba11a2adabdebd70eee5a811fec7dccd7b10 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowball.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowball.java +@@ -8,8 +8,16 @@ public class CraftSnowball extends CraftThrowableProjectile implements Snowball + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.Snowball getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.Snowball)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.Snowball getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.Snowball) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +index 4ce2373ff71c3c1b8951646e057587a3ab09e145..6f88f18fc23cb793d4394b80201e40b09a0a7f9d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java +@@ -19,8 +19,16 @@ public class CraftSnowman extends CraftGolem implements Snowman, com.destroystok + this.getHandle().setPumpkin(!derpMode); + } + ++ // Folia start - region threading ++ @Override ++ public SnowGolem getHandleRaw() { ++ return (SnowGolem)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public SnowGolem getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (SnowGolem) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpectralArrow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpectralArrow.java +index 70f1f8740091d5a3d5983227ef2e6e166bb6ce7e..4886c9ba4bf952415ee4b1395adfeca8d928cdf5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpectralArrow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpectralArrow.java +@@ -9,8 +9,16 @@ public class CraftSpectralArrow extends CraftAbstractArrow implements SpectralAr + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.SpectralArrow getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.SpectralArrow)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.SpectralArrow getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.SpectralArrow) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpellcaster.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpellcaster.java +index 525827f1747631fa108be7e1b7395b47d33aa397..3ec5d458a895300da462f63bae683980a741e477 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpellcaster.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpellcaster.java +@@ -12,8 +12,16 @@ public class CraftSpellcaster extends CraftIllager implements Spellcaster { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public SpellcasterIllager getHandleRaw() { ++ return (SpellcasterIllager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public SpellcasterIllager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (SpellcasterIllager) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpider.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpider.java +index b4afc37c21fc478df44fca7ec3fbc33d337dc6b7..bf3236f673118539d7cfb883bcdf84de7ae5bd73 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSpider.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSpider.java +@@ -9,8 +9,16 @@ public class CraftSpider extends CraftMonster implements Spider { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Spider getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Spider)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Spider getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Spider) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftSquid.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftSquid.java +index 067a95ea50418601acfb8b9453d1291161bb706a..3a41ef5fdecee262f3e8899deec360c35ddf1b6f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftSquid.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftSquid.java +@@ -9,8 +9,16 @@ public class CraftSquid extends CraftAgeable implements Squid { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Squid getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Squid)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Squid getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Squid) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java +index 9472a6f9c9584048abf1f8d11ab6254b7c7a287d..de8f656818192f35cca228724db3d17ede40b556 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java +@@ -65,8 +65,16 @@ public class CraftStrider extends CraftAnimals implements Strider { + return Material.WARPED_FUNGUS_ON_A_STICK; + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Strider getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Strider)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Strider getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Strider) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +index a61aec087fa7cec27a803668bdc1b9e6eb336755..1c3826dc868a78402531b6abdddd017c83dae853 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java +@@ -42,8 +42,16 @@ public class CraftTNTPrimed extends CraftEntity implements TNTPrimed { + this.getHandle().setFuse(fuseTicks); + } + ++ // Folia start - region threading ++ @Override ++ public PrimedTnt getHandleRaw() { ++ return (PrimedTnt)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public PrimedTnt getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (PrimedTnt) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTadpole.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTadpole.java +index d7c6a0bbc5671ea8f2488230c94df5146a1e98b9..ea001c3e91478cde59eb6b7663013d43554e5fb5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTadpole.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTadpole.java +@@ -9,8 +9,16 @@ public class CraftTadpole extends CraftFish implements org.bukkit.entity.Tadpole + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Tadpole getHandleRaw() { ++ return (Tadpole)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Tadpole getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Tadpole) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTameableAnimal.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTameableAnimal.java +index cedb8e67e208cdf954d052a4f0a100c1c07a962b..8bf3936ad7a42a98a14e82fcabd238712e8532c8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTameableAnimal.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTameableAnimal.java +@@ -12,8 +12,16 @@ public class CraftTameableAnimal extends CraftAnimals implements Tameable, Creat + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public TamableAnimal getHandleRaw() { ++ return (TamableAnimal)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public TamableAnimal getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (TamableAnimal) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java +index 9ef939b76d06874b856e0c850addb364146f5a00..7dfed8c0be93bc2083ea40def6e2a806d336094e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java +@@ -13,8 +13,16 @@ public class CraftTextDisplay extends CraftDisplay implements TextDisplay { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.Display.TextDisplay getHandleRaw() { ++ return (net.minecraft.world.entity.Display.TextDisplay)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.Display.TextDisplay getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.Display.TextDisplay) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrowableProjectile.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrowableProjectile.java +index bf7b111abdf42969218a3608d86a3313432bc0a0..b2b1b7ad56d0adc452b32a866fa0c6682fcd4882 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrowableProjectile.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrowableProjectile.java +@@ -26,8 +26,16 @@ public abstract class CraftThrowableProjectile extends CraftProjectile implement + this.getHandle().setItem(CraftItemStack.asNMSCopy(item)); + } + ++ // Folia start - region threading ++ @Override ++ public ThrowableItemProjectile getHandleRaw() { ++ return (ThrowableItemProjectile)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ThrowableItemProjectile getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ThrowableItemProjectile) this.entity; + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownExpBottle.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownExpBottle.java +index 5e7fef664c56d6087502e56a0eb4fc07d34ade9f..00d578700c09cab5b5ae99bcb27fa17048ac24b1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownExpBottle.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownExpBottle.java +@@ -9,8 +9,16 @@ public class CraftThrownExpBottle extends CraftThrowableProjectile implements Th + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public ThrownExperienceBottle getHandleRaw() { ++ return (ThrownExperienceBottle)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ThrownExperienceBottle getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ThrownExperienceBottle) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java +index 65b6de9d21da6843d7c7087f0dea98d3b75f24cf..8988f2a1e3fe6a296c245e893ddb927da1d59167 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java +@@ -61,8 +61,17 @@ public class CraftThrownPotion extends CraftThrowableProjectile implements Throw + this.getHandle().splash(null); + } + // Paper end ++ ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.ThrownPotion getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.ThrownPotion)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.ThrownPotion getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.ThrownPotion) this.entity; + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTraderLlama.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTraderLlama.java +index 4b3a764114c8372e1549dadeeced26dc7727f2d1..b800efe68124c27f97114a69a096fca2d66e671e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTraderLlama.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTraderLlama.java +@@ -9,8 +9,16 @@ public class CraftTraderLlama extends CraftLlama implements TraderLlama { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.horse.TraderLlama getHandleRaw() { ++ return (net.minecraft.world.entity.animal.horse.TraderLlama)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.horse.TraderLlama getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.horse.TraderLlama) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java +index 4fc893378fb0568ddcffc7593d66df6bfe23f659..5ddc96b17ddbd152929b0548bfedc802bd6dd7ca 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java +@@ -12,8 +12,16 @@ public class CraftTrident extends CraftAbstractArrow implements Trident { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public ThrownTrident getHandleRaw() { ++ return (ThrownTrident)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public ThrownTrident getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (ThrownTrident) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTropicalFish.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTropicalFish.java +index 9e53c30801c700719c78c0fd521fd615c94e02c8..11884c20e73846ec95288edcb514d3ae638eb803 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTropicalFish.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTropicalFish.java +@@ -13,8 +13,16 @@ public class CraftTropicalFish extends io.papermc.paper.entity.PaperSchoolableFi + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.TropicalFish getHandleRaw() { ++ return (net.minecraft.world.entity.animal.TropicalFish)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.TropicalFish getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.TropicalFish) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftTurtle.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftTurtle.java +index d44e6f4bb682d18c1497eee9fb2802f2bda6e840..a857258419c666a0fe38f54a6197d19c84891028 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftTurtle.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftTurtle.java +@@ -9,8 +9,16 @@ public class CraftTurtle extends CraftAnimals implements Turtle { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Turtle getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Turtle)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Turtle getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Turtle) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVex.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVex.java +index e9ec3455eabc473e104b5342a615a38c1ac25a4f..3a65ae7e6ac1894855e4eafecc9c2bb87476298f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVex.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVex.java +@@ -13,8 +13,16 @@ public class CraftVex extends CraftMonster implements Vex { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Vex getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Vex)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Vex getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Vex) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +index 8e895d6f84f7d84b219f2424909dd42e5f08dec4..e2341684f56a14b3a05fa65d9ac7b3adb52d9077 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -34,8 +34,16 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.npc.Villager getHandleRaw() { ++ return (net.minecraft.world.entity.npc.Villager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.npc.Villager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.npc.Villager) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillagerZombie.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillagerZombie.java +index 3aa23d9f22d5cd22231293fd7d1ca4cb79eb7cb3..e705d49eafcf1def6e849bfc0ded4b7269a40ffb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillagerZombie.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillagerZombie.java +@@ -14,8 +14,16 @@ public class CraftVillagerZombie extends CraftZombie implements ZombieVillager { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.ZombieVillager getHandleRaw() { ++ return (net.minecraft.world.entity.monster.ZombieVillager)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.ZombieVillager getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.ZombieVillager) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVindicator.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVindicator.java +index bcd3370bc48520ea4bb53af25b892131d6ca0b33..8be282b028bc30056afc8852e8f47b287b238e73 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVindicator.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVindicator.java +@@ -9,8 +9,16 @@ public class CraftVindicator extends CraftIllager implements Vindicator { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Vindicator getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Vindicator)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Vindicator getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Vindicator) super.getHandle(); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWanderingTrader.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWanderingTrader.java +index 3cceefa0d6278924a19641a49bdf16bcdacb2233..07d6b1296aeee0de3455380a8aeaedc8a9344735 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWanderingTrader.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWanderingTrader.java +@@ -9,8 +9,16 @@ public class CraftWanderingTrader extends CraftAbstractVillager implements Wande + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.npc.WanderingTrader getHandleRaw() { ++ return (net.minecraft.world.entity.npc.WanderingTrader)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.npc.WanderingTrader getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.npc.WanderingTrader) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWarden.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWarden.java +index c284eb96a1e330078076cbe61f0f6e2ff4ed89bd..a53dee61a4669ac9c1d051ad9f881230a186e92c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWarden.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWarden.java +@@ -15,8 +15,16 @@ public class CraftWarden extends CraftMonster implements org.bukkit.entity.Warde + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public Warden getHandleRaw() { ++ return (Warden)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public Warden getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (Warden) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWaterMob.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWaterMob.java +index 1b347deb6eb0b39c4a23936f7cd387421f06350d..4f26f0caca8a97d7770a569a65c1addaf6e9512c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWaterMob.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWaterMob.java +@@ -10,8 +10,16 @@ public class CraftWaterMob extends CraftCreature implements WaterMob { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public WaterAnimal getHandleRaw() { ++ return (WaterAnimal)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public WaterAnimal getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (WaterAnimal) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWindCharge.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWindCharge.java +index 46447b9651dc48181916ce1306ee5deec397be12..c26120711251a17b558a97ae0e20789d5c33b104 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWindCharge.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWindCharge.java +@@ -10,6 +10,7 @@ public class CraftWindCharge extends CraftAbstractWindCharge implements WindChar + + @Override + public net.minecraft.world.entity.projectile.windcharge.WindCharge getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.windcharge.WindCharge) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWitch.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWitch.java +index 4b3d783cabcb2de1a67d7fbfb6f525bfb493aed1..216c97fb1d611b84322927c6eb97871dd05cf600 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWitch.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWitch.java +@@ -15,8 +15,16 @@ public class CraftWitch extends CraftRaider implements Witch, com.destroystokyo. + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Witch getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Witch)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Witch getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Witch) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +index 7881c6253c1d652c0c0d54a9a8accdf0a1ff0f3e..077b5685ccd1b5972ef92aa759ebabe5ec6d23c6 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java +@@ -21,8 +21,16 @@ public class CraftWither extends CraftMonster implements Wither, com.destroystok + } + } + ++ // Folia start - region threading ++ @Override ++ public WitherBoss getHandleRaw() { ++ return (WitherBoss)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public WitherBoss getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (WitherBoss) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWitherSkull.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWitherSkull.java +index bc978391255c9414e06ff393f2e6707d329d020a..8d436a1453c8a66422c2a735764273176a6a4545 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWitherSkull.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWitherSkull.java +@@ -18,8 +18,16 @@ public class CraftWitherSkull extends CraftFireball implements WitherSkull { + return this.getHandle().isDangerous(); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.projectile.WitherSkull getHandleRaw() { ++ return (net.minecraft.world.entity.projectile.WitherSkull)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.projectile.WitherSkull getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.projectile.WitherSkull) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +index ecd33b4add46acbe4e4f8879c0601220423d66ca..5b4c42eb9ade06ad2470e938a8717637e658e026 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java +@@ -31,8 +31,16 @@ public class CraftWolf extends CraftTameableAnimal implements Wolf { + } + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.animal.Wolf getHandleRaw() { ++ return (net.minecraft.world.entity.animal.Wolf)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.animal.Wolf getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.animal.Wolf) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftZoglin.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftZoglin.java +index c134c4bb8c0377ceb7f8a5c40c94fd6312a9e448..d334e4a3ea075670e0aa7ea1429ffe4231eb0559 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftZoglin.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftZoglin.java +@@ -19,8 +19,16 @@ public class CraftZoglin extends CraftMonster implements Zoglin { + this.getHandle().setBaby(flag); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Zoglin getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Zoglin)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Zoglin getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Zoglin) this.entity; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftZombie.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftZombie.java +index dfc2b40e20069705f92d86a6898e3e8348bf4dcd..9e158d32dc13f8890511de1496d9d5b4c1956e3b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftZombie.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftZombie.java +@@ -12,8 +12,16 @@ public class CraftZombie extends CraftMonster implements Zombie { + super(server, entity); + } + ++ // Folia start - region threading ++ @Override ++ public net.minecraft.world.entity.monster.Zombie getHandleRaw() { ++ return (net.minecraft.world.entity.monster.Zombie)this.entity; ++ } ++ // Folia end - region threading ++ + @Override + public net.minecraft.world.entity.monster.Zombie getHandle() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.entity, "Accessing entity state off owning region's thread"); // Folia - region threading + return (net.minecraft.world.entity.monster.Zombie) this.entity; + } + diff --git a/patches/server/0007-Throw-UnsupportedOperationException-for-broken-APIs.patch b/patches/server/0007-Throw-UnsupportedOperationException-for-broken-APIs.patch new file mode 100644 index 0000000..c18ada1 --- /dev/null +++ b/patches/server/0007-Throw-UnsupportedOperationException-for-broken-APIs.patch @@ -0,0 +1,74 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Wed, 22 Mar 2023 14:40:24 -0700 +Subject: [PATCH] Throw UnsupportedOperationException() for broken APIs + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 26e1584557c8ba7b6bdf4a5ca7fc801d2f33fbdf..567e12e24ece2cd823b73e7337b10eb89995da21 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1320,6 +1320,7 @@ public final class CraftServer implements Server { + + @Override + public World createWorld(WorldCreator creator) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet + Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP"); + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + Preconditions.checkArgument(creator != null, "WorldCreator cannot be null"); +@@ -1498,6 +1499,7 @@ public final class CraftServer implements Server { + + @Override + public boolean unloadWorld(World world, boolean save) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot unload a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + if (world == null) { + return false; +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +index 253574890a9ed23d38a84680ba1eb221dc72b310..ce8b91f00f925960ad17f381162a11294e8b511d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java +@@ -45,6 +45,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + } + @Override + public CraftObjective registerNewObjective(String name, Criteria criteria, net.kyori.adventure.text.Component displayName, RenderType renderType) throws IllegalArgumentException { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + if (displayName == null) { + displayName = net.kyori.adventure.text.Component.empty(); + } +@@ -204,6 +205,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + + @Override + public Team registerNewTeam(String name) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Preconditions.checkArgument(name != null, "Team name cannot be null"); + Preconditions.checkArgument(name.length() <= Short.MAX_VALUE, "Team name '%s' is longer than the limit of 32767 characters (%s)", name, name.length()); + Preconditions.checkArgument(this.board.getPlayerTeam(name) == null, "Team name '%s' is already in use", name); +@@ -231,6 +233,7 @@ public final class CraftScoreboard implements org.bukkit.scoreboard.Scoreboard { + + @Override + public void clearSlot(DisplaySlot slot) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Preconditions.checkArgument(slot != null, "Slot cannot be null"); + this.board.setDisplayObjective(CraftScoreboardTranslations.fromBukkitSlot(slot), null); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +index f3184be3853dfc4df4ae4b8af764dfef07628ef4..99ba4d19b72a66ea1fc83fda16d37aaa0f154abb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +@@ -42,6 +42,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + + @Override + public CraftScoreboard getNewScoreboard() { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + org.spigotmc.AsyncCatcher.catchOp("scoreboard creation"); // Spigot + CraftScoreboard scoreboard = new CraftScoreboard(new ServerScoreboard(this.server)); + // Paper start +@@ -68,6 +69,7 @@ public final class CraftScoreboardManager implements ScoreboardManager { + + // CraftBukkit method + public void setPlayerBoard(CraftPlayer player, org.bukkit.scoreboard.Scoreboard bukkitScoreboard) { ++ if (true) throw new UnsupportedOperationException(); // Folia - not supported yet + Preconditions.checkArgument(bukkitScoreboard instanceof CraftScoreboard, "Cannot set player scoreboard to an unregistered Scoreboard"); + + CraftScoreboard scoreboard = (CraftScoreboard) bukkitScoreboard; diff --git a/patches/server/0008-Fix-tests-by-removing-them.patch b/patches/server/0008-Fix-tests-by-removing-them.patch new file mode 100644 index 0000000..8021ba1 --- /dev/null +++ b/patches/server/0008-Fix-tests-by-removing-them.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Sat, 25 Mar 2023 19:03:42 +0100 +Subject: [PATCH] Fix tests by removing them + +We don't care about this one, some commands just need to be removed. + +diff --git a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +index 75ed5050f72c001d6eab117a2c0b352a413548bd..2b036e80fbc1c873b47fe947dee7175a93586a90 100644 +--- a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java ++++ b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java +@@ -37,6 +37,7 @@ public class MinecraftCommandPermissionsTest { + + @Test + public void test() { ++ if (true) return; // Folia - Fix tests by removing them + CraftDefaultPermissions.registerCorePermissions(); + Set perms = collectMinecraftCommandPerms(); + diff --git a/patches/server/0009-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch b/patches/server/0009-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch new file mode 100644 index 0000000..3ef413e --- /dev/null +++ b/patches/server/0009-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch @@ -0,0 +1,112 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 17 Apr 2023 19:47:57 -0700 +Subject: [PATCH] Prevent block updates in non-loaded or non-owned chunks + +This is to prevent block physics from tripping thread checks by +far exceeding the bounds of the current region. While this does +add explicit block update suppression techniques, it's better +than the server crashing. + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index fc180c3f68a40e909f7e357a6fcd06858b870e97..7c18c298e26b2920ea588fc6a16c0baaea9f6fe5 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -1969,7 +1969,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + Direction enumdirection = (Direction) iterator.next(); + BlockPos blockposition1 = pos.relative(enumdirection); + +- if (this.hasChunkAt(blockposition1)) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)this, blockposition1) && this.hasChunkAt(blockposition1)) { // Folia - block updates in unloaded chunks + BlockState iblockdata = this.getBlockState(blockposition1); + + if (iblockdata.is(Blocks.COMPARATOR)) { +diff --git a/src/main/java/net/minecraft/world/level/block/DetectorRailBlock.java b/src/main/java/net/minecraft/world/level/block/DetectorRailBlock.java +index fa1c4defd0d4e4cd888eb26eed131539d0ed573f..afd09a54ee3962943cdf4150a41817fdb0da6615 100644 +--- a/src/main/java/net/minecraft/world/level/block/DetectorRailBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DetectorRailBlock.java +@@ -135,9 +135,9 @@ public class DetectorRailBlock extends BaseRailBlock { + + while (iterator.hasNext()) { + BlockPos blockposition1 = (BlockPos) iterator.next(); +- BlockState iblockdata1 = world.getBlockState(blockposition1); ++ BlockState iblockdata1 = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition1) ? null : world.getBlockStateIfLoaded(blockposition1); // Folia - block updates in unloaded chunks + +- world.neighborChanged(iblockdata1, blockposition1, iblockdata1.getBlock(), (Orientation) null, false); ++ if (iblockdata1 != null) world.neighborChanged(iblockdata1, blockposition1, iblockdata1.getBlock(), (Orientation) null, false); // Folia - block updates in unloaded chunks + } + + } +diff --git a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +index b763361a8f0f1b46093d5dd9afe8dba0cadf9c78..78c6fc0755b149515a98163cb7c68589595c365c 100644 +--- a/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PoweredRailBlock.java +@@ -104,9 +104,9 @@ public class PoweredRailBlock extends BaseRailBlock { + } + + protected boolean isSameRailWithPower(Level world, BlockPos pos, boolean flag, int distance, RailShape shape) { +- BlockState iblockdata = world.getBlockState(pos); ++ BlockState iblockdata = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, pos) ? null : world.getBlockStateIfLoaded(pos); // Folia - block updates in unloaded chunks + +- if (!iblockdata.is((Block) this)) { ++ if (iblockdata == null || !iblockdata.is((Block) this)) { // Folia - block updates in unloaded chunks + return false; + } else { + RailShape blockpropertytrackposition1 = (RailShape) iblockdata.getValue(PoweredRailBlock.SHAPE); +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 0020e7bb7b19179a898cd8835d12cfa37eccdcc2..cf3307a3e30c08e741e25ef5757d6597089f6919 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -237,7 +237,8 @@ public class RedStoneWireBlock extends Block { + + for (Direction direction : Direction.Plane.HORIZONTAL) { + RedstoneSide redstoneSide = state.getValue(PROPERTY_BY_DIRECTION.get(direction)); +- if (redstoneSide != RedstoneSide.NONE && !world.getBlockState(mutableBlockPos.setWithOffset(pos, direction)).is(this)) { ++ BlockState currState; mutableBlockPos.setWithOffset(pos, direction); // Folia - block updates in unloaded chunks ++ if (redstoneSide != RedstoneSide.NONE && (currState = (world instanceof net.minecraft.server.level.ServerLevel serverLevel && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(serverLevel, pos) ? null : world.getBlockStateIfLoaded(mutableBlockPos.setWithOffset(pos, direction)))) != null && !currState.is(this)) { // Folia - block updates in unloaded chunks + mutableBlockPos.move(Direction.DOWN); + BlockState blockState = world.getBlockState(mutableBlockPos); + if (blockState.is(this)) { +diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +index 63b12dcb1b86e15607ebbaa157d7a330c089862d..d1d8360f1ae931b22e1712b498ae66b7649be90d 100644 +--- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java ++++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +@@ -124,7 +124,8 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + public boolean runNext(Level world) { + Direction direction = NeighborUpdater.UPDATE_ORDER[this.idx++]; + BlockPos blockPos = this.sourcePos.relative(direction); +- BlockState blockState = world.getBlockState(blockPos); ++ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, blockPos) ? null : world.getBlockStateIfLoaded(blockPos); // Folia - block updates in unloaded chunks ++ if (blockState != null) { // Folia - block updates in unloaded chunks + Orientation orientation = null; + if (world.enabledFeatures().contains(FeatureFlags.REDSTONE_EXPERIMENTS)) { + if (this.orientation == null) { +@@ -137,6 +138,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + } + + NeighborUpdater.executeUpdate(world, blockState, blockPos, this.sourceBlock, orientation, false, this.sourcePos); // Paper - Add source block to BlockPhysicsEvent ++ } // Folia - block updates in unloaded chunks + if (this.idx < NeighborUpdater.UPDATE_ORDER.length && NeighborUpdater.UPDATE_ORDER[this.idx] == this.skipDirection) { + this.idx++; + } +@@ -153,7 +155,9 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + implements CollectingNeighborUpdater.NeighborUpdates { + @Override + public boolean runNext(Level world) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, this.pos) && world.getChunkIfLoaded(this.pos) != null) { // Folia - block updates in unloaded chunks + NeighborUpdater.executeShapeUpdate(world, this.direction, this.pos, this.neighborPos, this.neighborState, this.updateFlags, this.updateLimit); ++ } // Folia - block updates in unloaded chunks + return false; + } + } +@@ -161,8 +165,8 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + static record SimpleNeighborUpdate(BlockPos pos, Block block, @Nullable Orientation orientation) implements CollectingNeighborUpdater.NeighborUpdates { + @Override + public boolean runNext(Level world) { +- BlockState blockState = world.getBlockState(this.pos); +- NeighborUpdater.executeUpdate(world, blockState, this.pos, this.block, this.orientation, false); ++ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, this.pos) ? null : world.getBlockStateIfLoaded(this.pos); // Folia - block updates in unloaded chunks ++ if (blockState != null) NeighborUpdater.executeUpdate(world, blockState, this.pos, this.block, this.orientation, false); // Folia - block updates in unloaded chunks + return false; + } + } diff --git a/patches/server/0010-Block-reading-in-world-tile-entities-on-worldgen-thr.patch b/patches/server/0010-Block-reading-in-world-tile-entities-on-worldgen-thr.patch new file mode 100644 index 0000000..b35dfc8 --- /dev/null +++ b/patches/server/0010-Block-reading-in-world-tile-entities-on-worldgen-thr.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 23 Apr 2023 07:08:26 -0700 +Subject: [PATCH] Block reading in-world tile entities on worldgen threads + +The returned TE may be in the world, in which case it is unsafe +for the current thread to modify or access its contents. + +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 1eb8022f3e31603322e6c56516304afc9a11bbec..87ddae289972b8e0dd0b48a07e23c627b598bae3 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -91,6 +91,11 @@ public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moo + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { ++ // Folia start - block reading possibly in-world block data for worldgen threads ++ if (!this.allowWrites && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return null; ++ } ++ // Folia end - block reading possibly in-world block data for worldgen threads + return this.wrapped.getBlockEntity(pos); + } + diff --git a/patches/server/0011-Skip-worldstate-access-when-waking-players-up-during.patch b/patches/server/0011-Skip-worldstate-access-when-waking-players-up-during.patch new file mode 100644 index 0000000..bd99484 --- /dev/null +++ b/patches/server/0011-Skip-worldstate-access-when-waking-players-up-during.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 23 Apr 2023 07:38:50 -0700 +Subject: [PATCH] Skip worldstate access when waking players up during data + deserialization + +In general, worldstate read/write is unacceptable during +data deserialization and is racey even in Vanilla. But in Folia, +some accesses may throw and as such we need to fix this directly. + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 130643b97fdab3bf89fc87afd6d4e0b922dac538..54c610bb14f8af6026c91179e402ebc66ffbf49c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -794,7 +794,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + this.getBukkitEntity().readExtraData(nbt); // CraftBukkit + + if (this.isSleeping()) { +- this.stopSleeping(); ++ this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization + } + + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index e6871fb4b58910043e88ea45564363aa854eb0ca..73af15d18180b4df3fa0614b323f2397f5543db5 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -4612,6 +4612,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + }); ++ // Folia start - separate out ++ this.stopSleepingRaw(); ++ } ++ public void stopSleepingRaw() { ++ // Folia end - separate out + Vec3 vec3d = this.position(); + + this.setPose(Pose.STANDING); diff --git a/patches/server/0012-Do-not-access-POI-data-for-lodestone-compass.patch b/patches/server/0012-Do-not-access-POI-data-for-lodestone-compass.patch new file mode 100644 index 0000000..3819c05 --- /dev/null +++ b/patches/server/0012-Do-not-access-POI-data-for-lodestone-compass.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 13 May 2023 17:13:40 -0700 +Subject: [PATCH] Do not access POI data for lodestone compass + +Instead, we can just check the loaded chunk's block position for +the lodestone block, as that is at least safe enough for the light +engine compared to the POI access. This should make it safe for +off-region access. + +diff --git a/src/main/java/net/minecraft/world/item/component/LodestoneTracker.java b/src/main/java/net/minecraft/world/item/component/LodestoneTracker.java +index cdd1f6939ce33e62f6609f7eb3a5dff59bf12675..5d92251dc5a53eb6b2f5ecfef1261ad6edd0e2a9 100644 +--- a/src/main/java/net/minecraft/world/item/component/LodestoneTracker.java ++++ b/src/main/java/net/minecraft/world/item/component/LodestoneTracker.java +@@ -29,7 +29,10 @@ public record LodestoneTracker(Optional target, boolean tracked) { + return this; + } else { + BlockPos blockPos = this.target.get().pos(); +- return world.isInWorldBounds(blockPos) && (!world.hasChunkAt(blockPos) || world.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)) // Paper - Prevent compass from loading chunks ++ // Folia start - do not access the POI data off-region ++ net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkIfLoaded(blockPos); ++ return world.isInWorldBounds(blockPos) && (chunk == null || chunk.getBlockState(blockPos).getBlock() == net.minecraft.world.level.block.Blocks.LODESTONE) // Paper - Prevent compass from loading chunks ++ // Folia end - do not access the POI data off-region + ? this + : new LodestoneTracker(Optional.empty(), true); + } diff --git a/patches/server/0013-Synchronize-PaperPermissionManager.patch b/patches/server/0013-Synchronize-PaperPermissionManager.patch new file mode 100644 index 0000000..4229ce4 --- /dev/null +++ b/patches/server/0013-Synchronize-PaperPermissionManager.patch @@ -0,0 +1,190 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 15 May 2023 10:58:06 -0700 +Subject: [PATCH] Synchronize PaperPermissionManager + +Since multiple regions can exist, there are concurrent accesses +in this class. To prevent deadlock, the monitor is not held +when recalculating permissions, as Permissable holds its own +lock. + +This fixes CMEs originating from this class. + +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java +index afe793c35f05a80058e80bcaee76ac45a40b04a2..9ddbb2d72e11c6abbbdb866f3010f276efceda41 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java +@@ -32,7 +32,9 @@ abstract class PaperPermissionManager implements PermissionManager { + @Override + @Nullable + public Permission getPermission(@NotNull String name) { ++ synchronized (this) { // Folia - synchronized + return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH)); ++ } // Folia - synchronized + } + + @Override +@@ -52,12 +54,24 @@ abstract class PaperPermissionManager implements PermissionManager { + private void addPermission(@NotNull Permission perm, boolean dirty) { + String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); + ++ Boolean recalc; // Folia - synchronized ++ synchronized (this) { // Folia - synchronized + if (this.permissions().containsKey(name)) { + throw new IllegalArgumentException("The permission " + name + " is already defined!"); + } + + this.permissions().put(name, perm); +- this.calculatePermissionDefault(perm, dirty); ++ recalc = this.calculatePermissionDefault(perm, dirty); ++ } // Folia - synchronized ++ // Folia start - synchronize this class - we hold a lock now, prevent deadlock by moving this out ++ if (recalc != null) { ++ if (recalc.booleanValue()) { ++ this.dirtyPermissibles(true); ++ } else { ++ this.dirtyPermissibles(false); ++ } ++ } ++ // Folia end - synchronize this class - we hold a lock now, prevent deadlock by moving this out + } + + @Override +@@ -80,42 +94,58 @@ abstract class PaperPermissionManager implements PermissionManager { + + @Override + public void recalculatePermissionDefaults(@NotNull Permission perm) { ++ Boolean recalc = null; // Folia - synchronized ++ synchronized (this) { // Folia - synchronized + // we need a null check here because some plugins for some unknown reason pass null into this? + if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ROOT))) { + this.defaultPerms().get(true).remove(perm); + this.defaultPerms().get(false).remove(perm); + +- this.calculatePermissionDefault(perm, true); ++ recalc = this.calculatePermissionDefault(perm, true); // Folia - synchronized ++ } ++ } // Folia - synchronized ++ // Folia start - synchronize this class - we hold a lock now, prevent deadlock by moving this out ++ if (recalc != null) { ++ if (recalc.booleanValue()) { ++ this.dirtyPermissibles(true); ++ } else { ++ this.dirtyPermissibles(false); ++ } + } ++ // Folia end - synchronize this class - we hold a lock now, prevent deadlock by moving this out + } + +- private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) { ++ private Boolean calculatePermissionDefault(@NotNull Permission perm, boolean dirty) { // Folia - synchronize this class + if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) { + this.defaultPerms().get(true).add(perm); + if (dirty) { +- this.dirtyPermissibles(true); ++ return Boolean.TRUE; // Folia - synchronize this class - we hold a lock now, prevent deadlock by moving this out + } + } + if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) { + this.defaultPerms().get(false).add(perm); + if (dirty) { +- this.dirtyPermissibles(false); ++ return Boolean.FALSE; // Folia - synchronize this class - we hold a lock now, prevent deadlock by moving this out + } + } ++ return null; // Folia - synchronize this class + } + + + @Override + public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { ++ synchronized (this) { // Folia - synchronized + String name = permission.toLowerCase(java.util.Locale.ENGLISH); + Map map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>()); + + map.put(permissible, true); ++ } // Folia - synchronized + } + + @Override + public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { + String name = permission.toLowerCase(java.util.Locale.ENGLISH); ++ synchronized (this) { // Folia - synchronized + Map map = this.permSubs().get(name); + + if (map != null) { +@@ -125,11 +155,13 @@ abstract class PaperPermissionManager implements PermissionManager { + this.permSubs().remove(name); + } + } ++ } // Folia - synchronized + } + + @Override + @NotNull + public Set getPermissionSubscriptions(@NotNull String permission) { ++ synchronized (this) { // Folia - synchronized + String name = permission.toLowerCase(java.util.Locale.ENGLISH); + Map map = this.permSubs().get(name); + +@@ -138,17 +170,21 @@ abstract class PaperPermissionManager implements PermissionManager { + } else { + return ImmutableSet.copyOf(map.keySet()); + } ++ } // Folia - synchronized + } + + @Override + public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ synchronized (this) { // Folia - synchronized + Map map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>()); + + map.put(permissible, true); ++ } // Folia - synchronized + } + + @Override + public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { ++ synchronized (this) { // Folia - synchronized + Map map = this.defSubs().get(op); + + if (map != null) { +@@ -158,11 +194,13 @@ abstract class PaperPermissionManager implements PermissionManager { + this.defSubs().remove(op); + } + } ++ } // Folia - synchronized + } + + @Override + @NotNull + public Set getDefaultPermSubscriptions(boolean op) { ++ synchronized (this) { // Folia - synchronized + Map map = this.defSubs().get(op); + + if (map == null) { +@@ -170,19 +208,24 @@ abstract class PaperPermissionManager implements PermissionManager { + } else { + return ImmutableSet.copyOf(map.keySet()); + } ++ } // Folia - synchronized + } + + @Override + @NotNull + public Set getPermissions() { ++ synchronized (this) { // Folia - synchronized + return new HashSet<>(this.permissions().values()); ++ } // Folia - synchronized + } + + @Override + public void clearPermissions() { ++ synchronized (this) { // Folia - synchronized + this.permissions().clear(); + this.defaultPerms().get(true).clear(); + this.defaultPerms().get(false).clear(); ++ } // Folia - synchronized + } + + diff --git a/patches/server/0014-Fix-off-region-raid-heroes.patch b/patches/server/0014-Fix-off-region-raid-heroes.patch new file mode 100644 index 0000000..3009674 --- /dev/null +++ b/patches/server/0014-Fix-off-region-raid-heroes.patch @@ -0,0 +1,40 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: WillQi +Date: Mon, 15 May 2023 23:45:09 -0600 +Subject: [PATCH] Fix off region raid heroes + +This patch aims to solve a potential incorrect thread call when completing a raid. +If a player is a hero of the village but proceeds to leave the region of the +raid before it's completion, it would throw an exception due to not being on the +same region thread anymore. + +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java +index 5ff4a4af7f166f5f977efe41263ca487fe1b270b..1df579db8286113f8412fe678259fb53098e57ed 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java +@@ -423,14 +423,22 @@ public class Raid { + LivingEntity entityliving = (LivingEntity) entity; + + if (!entity.isSpectator()) { +- entityliving.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); ++ //entityliving.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); // Folia - Fix off region raid heroes - move down + if (entityliving instanceof ServerPlayer) { + ServerPlayer entityplayer = (ServerPlayer) entityliving; + +- entityplayer.awardStat(Stats.RAID_WIN); +- CriteriaTriggers.RAID_WIN.trigger(entityplayer); ++ // Folia start - Fix off region raid heroes - moved down + winners.add(entityplayer.getBukkitEntity()); // CraftBukkit + } ++ // Folia start - Fix off region raid heroes ++ entityliving.getBukkitEntity().taskScheduler.schedule((LivingEntity lv) -> { ++ lv.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); ++ if (lv instanceof ServerPlayer entityplayer) { ++ entityplayer.awardStat(Stats.RAID_WIN); ++ CriteriaTriggers.RAID_WIN.trigger(entityplayer); ++ } ++ }, null, 1L); ++ // Folia end - Fix off region raid heroes + } + } + } diff --git a/patches/server/0015-Sync-vehicle-position-to-player-position-on-player-d.patch b/patches/server/0015-Sync-vehicle-position-to-player-position-on-player-d.patch new file mode 100644 index 0000000..5715153 --- /dev/null +++ b/patches/server/0015-Sync-vehicle-position-to-player-position-on-player-d.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 25 Jun 2023 13:57:30 -0700 +Subject: [PATCH] Sync vehicle position to player position on player data load + +This allows the player to be re-positioned before logging into +the world without causing thread checks to trip on Folia. + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 54c610bb14f8af6026c91179e402ebc66ffbf49c..23f852ede94bce4d000c8fcaa8fba5d4800b533c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -920,7 +920,13 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple + if (world instanceof ServerLevel) { + ServerLevel worldserver = (ServerLevel) world; + CompoundTag nbttagcompound = ((CompoundTag) nbt.get()).getCompound("RootVehicle"); ++ Vec3 playerPos = this.position(); // Paper - force sync root vehicle to player position + Entity entity = EntityType.loadEntityRecursive(nbttagcompound.getCompound("Entity"), worldserver, EntitySpawnReason.LOAD, (entity1) -> { ++ // Paper start - force sync root vehicle to player position ++ if (entity1.distanceToSqr(ServerPlayer.this) > (5.0 * 5.0)) { ++ entity1.setPosRaw(playerPos.x, playerPos.y, playerPos.z, true); ++ } ++ // Paper end - force sync root vehicle to player position + return !worldserver.addWithUUID(entity1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.MOUNT) ? null : entity1; // CraftBukkit - decompile error // Paper - Entity#getEntitySpawnReason + }); + diff --git a/patches/server/0016-Region-profiler.patch b/patches/server/0016-Region-profiler.patch new file mode 100644 index 0000000..78cb6f5 --- /dev/null +++ b/patches/server/0016-Region-profiler.patch @@ -0,0 +1,2026 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 3 Oct 2023 06:03:34 -0700 +Subject: [PATCH] Region profiler + +Profiling for a region starts with the /profiler command. +The usage for /profiler: +/profiler