mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-17 04:13:46 +00:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0193f75daa | |||
| e8dd4e1127 | |||
| 699247b1de | |||
| 55b7460b9a | |||
| 45230c0689 | |||
| 4dea984d77 | |||
| cd05fd1bca | |||
| e42ab6fae6 | |||
| 402bcb04fe | |||
| 6111b5670a | |||
| 0a7a531646 | |||
| d1fec88769 | |||
| 848e056bb8 | |||
| 6b64624233 | |||
| d1cc83a40b | |||
| 927c48f8c7 | |||
| 29ff3f01a7 | |||
| a8e46e3590 | |||
| 75e8c22268 | |||
| f777ea6b18 | |||
| 8fb627f9c1 | |||
| 3cfbcbf8ed | |||
| 79e80bef33 | |||
| 796d81e58b | |||
| b0a5d97e9c | |||
| ad6732812e | |||
| f482b8ef7e | |||
| e2f6061f98 | |||
| b98a341dc7 | |||
| 261a850af4 | |||
| 516537db72 | |||
| 921a98f210 | |||
| 2bac948edb | |||
| 26d9115904 | |||
| 763ff90a61 | |||
| f32094a404 | |||
| 6b24105640 | |||
| a3d7269a99 | |||
| e1196b0364 | |||
| 4c462ba78a | |||
| 65d4abe5c0 | |||
| 5142c048c7 | |||
| 88c87175b3 | |||
| 56a0c75a39 | |||
| 704c94aab4 | |||
| b5d08b6203 | |||
| b584cb4193 | |||
| af3035088a | |||
| df888e5516 | |||
| ca10edef0c | |||
| 986c7f12cd | |||
| 6c5e991051 | |||
| 1a834ed6f6 | |||
| c9929e0880 | |||
| 3063801a3a | |||
| 13002b04df | |||
| 1c9f9d92bb | |||
| 4568f0b4b6 | |||
| 1c08eb23b2 | |||
| 7e224325f3 | |||
| a1ff3b2c55 | |||
| abfc122171 | |||
| 6a68884ce9 | |||
| c5a2a57822 | |||
| 58d24ca55e | |||
| c6c02a9040 | |||
| a530a03459 | |||
| a82b8f4a9c | |||
| cf2e7df298 | |||
| d20b3af9b9 | |||
| 01d98d2141 | |||
| febe930db0 | |||
| 5c832ba128 | |||
| 03f9e233da | |||
| 11efae428f | |||
| 1a6fff35e8 | |||
| 540a416bc9 | |||
| 7eb6e87a81 | |||
| 9ff1c502ef | |||
| 08c6f7ddf9 | |||
| 4bc1913f03 | |||
| 821cc027db | |||
| 1968ef2f2c | |||
| ce29b70618 | |||
| 3d128b70a7 | |||
| 23fad24fb7 | |||
| 6df718e6ca | |||
| 787c728060 | |||
| 88bbce82fe | |||
| e6a8351e57 | |||
| b82472d521 | |||
| 8c05f1bf1d | |||
| 6a5616abae | |||
| 6096c39192 | |||
| b42d0e4710 | |||
| dcb3306197 | |||
| 9a231b2bcf | |||
| aa706d027b | |||
| 568fb07f66 | |||
| bf207b7062 | |||
| 18d4dce1db | |||
| 651dfa247e | |||
| 5b1ff6d2f7 | |||
| 589baafc28 | |||
| 130073989d | |||
| 2ca8cc7ad3 | |||
| 9f35855599 | |||
| 5110a33857 | |||
| a38e87cce3 | |||
| 1d09cd5f0f | |||
| 72c891ce5b | |||
| 3964185a81 | |||
| afc5f67bc2 | |||
| 1c72d6410f | |||
| c2c612eb35 | |||
| 415c2f5837 | |||
| b491d9efd0 | |||
| d643461e2e | |||
| 1f41e195cf | |||
| 7b9280083f | |||
| 73525c170d | |||
| 16c861a781 | |||
| d6bd5ec82f | |||
| 1829fa3eb1 | |||
| 8d215e3911 | |||
| ba819e4899 | |||
| 05d79b6d40 | |||
| cf64fbb730 | |||
| 43a65135b1 | |||
| 440c1bec6c | |||
| 0fd8bcc488 | |||
| bb1c0b8f68 | |||
| fc25acdea2 | |||
| 0aa6f6d523 | |||
| a79c1a7954 | |||
| 7885762cd7 | |||
| 86ed3f0095 | |||
| 99fcc8fd03 | |||
| 41888e33f6 |
@@ -0,0 +1,31 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify (core, plugin, spi, probes)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Run verification gates
|
||||
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Run modded shared tests
|
||||
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
+34
-1
@@ -10,4 +10,37 @@ libs/
|
||||
|
||||
collection/
|
||||
|
||||
/core/src/main/java/com/volmit/iris/util/uniques/
|
||||
out/
|
||||
tools/simd-bench/out/
|
||||
tools/simd-bench/simd-bench.jar
|
||||
|
||||
DataPackExamples/
|
||||
|
||||
TreeGenStuff/
|
||||
dist/
|
||||
core/plugins/
|
||||
adapters/bukkit/plugin/plugins/
|
||||
adapters/*/run/
|
||||
|
||||
.codegraph/
|
||||
|
||||
plans/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
local.properties
|
||||
*.jks
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pem
|
||||
*.key
|
||||
*.hprof
|
||||
credentials.json
|
||||
service-account*.json
|
||||
|
||||
docs/
|
||||
|
||||
CROSSPLATFORM_PLAN.md
|
||||
|
||||
.qa/
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
#iris-goldenhash v1
|
||||
#world=goldentest
|
||||
#dim=overworld
|
||||
#seed=1337
|
||||
#mc=26.1.2.build.2591-stable
|
||||
#minY=-256 maxY=512
|
||||
#center=0,0
|
||||
#radius=8
|
||||
-8 0 e8c6f44099c05ed375fc14866389bc8c7c14b51b69434c9c516caf53d031c415 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 1 c555764a99e9bb25c1ad95890f24b38447d4104637eb8d1c84629b929dbef7ff 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 2 0d7bfb60c8913b6ee0225faa06c5759c2c1c05646ed2bac3fc924b3e53c0a125 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 3 f682f9939349330bfe2160d9b8d9ce0d43b3c710860107abce8d98a38ee85b7e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 4 e96d9e6b17b59f251023cb70f78a2e4a155d613ddd9208c0bb66c01589f3649c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 5 f3dc77a9e64d2d9639d7f23222bc4c61272e8dab1c93e97420d9667640e6d212 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 6 ad62cd0be871a2bb2d547e64df2ea40b40e38e729b19cbb21d7343d8146dfc51 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 7 4f01547457c8cb8da9b4de50780c8ceffbe8da3781cb171c682e367a46a4006a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 8 0fc9c9d510b070d6b8bc7bd867134f1367a3f5ad0514ffd8925ce089de734cb3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -8 01347120983abadd10251a68a3ea131414986aadc5861b9ed2684930998b187f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -7 65d0f851d4c2467f0f541967b692cd44d95ddb59cd2250af08bfa788af8a3803 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -6 d1bc22d2912b8555226c19f38f5f8e574331be5901215122a5ffbeb73b419765 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -5 ba6f58e5f76cbee8058c839ff7e42f4e7c4a07158cf97f959e4f676d4a477b72 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -4 3e7d2602fff6f0ace932134462745586d7baaed6fbdc5d4285f71219c19d281d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -3 88c0654c7380d9b616079d7991b41677e7f4c46acc6dcad93024e2c2b0f72288 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -2 94d591ae106d14204ed910cc1de28de4040f853d6a9bd47fc7fdebec0324f6a9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-8 -1 26218d2073cbe0cc87e91e789f973444510a49c0e3c3c5f94f32febb1496288a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 0 4549aad82cdfa6356d7de621924f62ca950640e9b8f5e771964fb7a66d648009 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 1 89e7a6a250b09565b2d00050fac709bc6612e16c4a7e0d189595f460ef28f1eb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 2 2da2f65fa337c51d140f339af28faa2987dc09f793b7dd3dcbdb2f801cec76d7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 3 5d5ec222384ce07398e352f405f23dd3454277c8d5251c32d870659a8c52f6fc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 4 1839f53ff781c3d133d741d297527a6c864f185be77c6a79ec294de34bb86e45 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 5 9f6f17c5c3e12f1d94e6a7b8a81ebe082e55b548213c605c904554baa864c83d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 6 85482bebafc31f647b56d400e41df1c78555446df9d24c7c9baeaa0760f0ea1e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 7 bb9382d28c0841a6aa6ebfc35ab8df808bea54cad20c858fb58a2652a8ca3e9e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 8 68bba9b6d00765c098cb3d29907977198303c69a39410e5d46ca109d791d22de 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -8 2c622b6c55a20271a48874ae9e489ce3c938c634eb4cb2c317b66a21ecf36c76 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -7 1f29f9d700900e966d96f6425c28e6ac5c30f77d6d5bc8efca22937678f6fbfa 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -6 3b9e3e48cc43167ac8e659409c62ae9bd7cdddca74cf6efc23dfead2913dff2b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -5 1ed142541d6bb7b11909a69257678277546b72cfdf8a64f44a471f827bbb671a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -4 d7f808e9d725823ba2b68e54b1985f458ce180fc1c5dda3dac4add38d831a1ce 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -3 0971e32206dcb07b64ef050c23a06a354f3d776095e10c7e65ca44db999b868e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -2 25e93be610632dbb353b4673aaf02b17a8153332f5557617ddafbce5332b8bb2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-7 -1 92bb2c7632a54d837db26d79d114fa6626b16289ab9879eca89aa3030633813f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 0 7ecbcbb6be3e4e6d8449f90468d9969204b18fb9746ecad6319741f9f12c6fbc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 1 3898687b4624ebeeb6e61fc40f39d6b5842f25495bfe63dcf9a4733596bc3c82 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 2 0e3017098a07c8b596f4d3b221b9ebd0d564bb0e2776391031a716c2bf1d01b7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 3 126fba2b943704a1791648a76e100f5ea263920837e334ecb7d9188c3d76412d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 4 68d6139155e498e561efb77c9f57568684429ada7918c47f44f6a85041478220 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 5 b50c5089e48a84d456120884e90e6a17d8bb02478743a97a0beb1b4eb4fb9890 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 6 0317ed303e7ae37b236bfa0b86d6c00bc2c6b6423f2d1b0228a0f0e4b0f5b6d2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 7 330599a6b02758c52f13792e83173fb8beca89e94f26f7c12d82415d8a0c49e1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 8 e74f8d776ca319f992298dfb1d0545f9e3796d8c74ff59d0a57e8155eb5d955b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -8 f65ef781c13b78f1ec46d98b1396c8c716b3752da6315cc805359354917480ab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -7 efdef3ed4747552d65982c605365dc6e576ab9030643efe8b1bb2456be5a09a8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -6 06f8d72fc8281b0e0fbbbf8f8b91575a1db4af551dc87f459877e3eaeebf363c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -5 54211524408e92d4518e7409ebaa45647ce7a6645a34de338167d2ff213839f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -4 5540bded756e63b331df11f89f57566ba86b711a4b88fb5480e5dda9f2540586 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -3 601b734e87ee9a74173b780d566f85322c45bdaf03125bbb070e136c9dfe5007 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -2 1ca8f873f3369f53537cb7b5506f19e1c7ea4fd06c817616508b0c96103b26c4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-6 -1 ea159282ee7c77e10e8506feccc6f8d6798ac414737521565b0f604911939c1f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 0 c1c95112697de045a0add257e6e793f03f1592d451e774eac0757ac7155b48ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 1 a5ea2ca87fa5e0dceb6df886d926cd4559c667a83abd86415f1c27dc9ed81ee5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 2 dc7f16b61d9edc02d5249b8d2ee6354272b03db78af103b149d393517b84bda8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 3 cee16aa555acbdf22437047b4256f6c768a9a030118f5638a642917657c1ef55 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 4 67681fafbaea4803cc440ec1e8f65d743c3b8a46697be8976d413d32f7a3c72f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 5 fba2f12c44e498b0507d319a43c0f2c967ce63bb77793027deb6d1ca3e6e234a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 6 b72a83a562f7d9e2f684f89de7f2893f8c213e74aadb58cd186ba61c5dc7e45d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 7 7089ebbd8ee10a7d7fc335055a350072414b0ffde6c0b3033df6d1adff1d2169 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 8 185ff9a6c30cfb4b0536b484bced135792f00509751cd006df2c714f53fec7b2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -8 d1d70baf419895f95f0b4eeb26a7b7510015153c72d47a9a48105ac170688ce6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -7 e328bdfec9d335c3dc8079a54b28c776752870004058db0844b2b4ebfc025071 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -6 70d531e3e5930e2fe203a446f97720f94ac7ce8a7aa5b1f9be733d081b503593 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -5 bd2b0e65f1c27f14e168f12add29691b2375583e9967f6b85b7f0a4bf5dad848 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -4 327d84fbaf300d46540d858bf08df637404d80d4de3be3dd0baac2b4939df441 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -3 c50372e6900776f53862e713ba873fcaec8102d77120460599a75f5aec4ef4ea 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -2 81ce5424ec2ad445bd0158cbe57137d5a566726a705fe35c7538088bb986526a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-5 -1 ad141fd05004823307416d1d3b4261d39387f2dc6e52b38475e1d9fd526dfe81 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 0 86e9e5ce6e00100fbfb8ea5112a0d86a450fdc297c9e710dc5a26f727c865ecd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 1 27dc14d872694dc1502d2f9b84a5d1a5dafca624a3972ddad3f25941feaaaa73 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 2 bed812029e6fed53cb5abb9ee76f970c322a9028c6421c6334ade091f2d467f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 3 b8a6b54438b8980d0ecbe3c2d7d0aebeb2bca228c530651c76213b2f6be90af4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 4 dda62b17f47229a7e1d88f29aaea0c06a98456b3d11a6fcab848f61be6011bc9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 5 b4b40ef42ec4dc99fbc16149387750bf43a4c0111909693de2ea14b98eb631a0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 6 e946df48cd22fdef70b500519b5f83a7369a014e6e92521df3ef94305ba12541 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 7 9c19d9ed486cf5fd9a8786a0ae4bea850dec20e987b3f34683e09d829b6bff14 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 8 e1851ad3d1e45e2447e581670864f5b31b804727d36ee6ceed956701379b5931 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -8 5f604232c902d63a68d07da0d381d6c211a0fc5cc5132b57c63eee182b9bb570 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -7 039905dec3c6e4f37ee946e4b593411f8ccc0b359d2fb0a26ee111b6c79cf242 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -6 bbbc3fa281dabfc4301c21237b1a3b7fae9bec0ce10545fd566e0e00dcc80043 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -5 ba1603746f8a4016fe9d1e6f9e58fd49b57c08445e49434624dd3f005b120f18 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -4 4bcf6ab1960c59eac15286e5adf8efb5140626bd22225bb78bddfe43adb7c1e0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -3 1ca1fae7c121e55be9ca1e369fe58f09a61f13b981f189598eb46aac03ebadc5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -2 a3154b14c6c2472b9e3596bcee51372ddaaf2b2b4225090c77797612b73571cd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-4 -1 ede17ad65f03e78875b5a96aa33004e0900c0304d56c228cea550f414203c469 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 0 bd87aa58f3fb8d3b974fc46449c776e610f2aff7667419089563dac7c938ca0f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 1 4dc30c498d41c84f8f6deed9accb857c29d759b533bce9b48768092690ac0424 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 2 d46285266abb3d5ca853d70c5549c4fd0fc8f32905feb66a55aec573d045afab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 3 01d5a13a92f8221047bf8755efbb4a42a6a68582f0bae4e3b081b7c306632bbd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 4 597746d886a37cdc7d875c6a0df97c3e7d8980d0ea33915fe307828f23686f1a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 5 3510c1c85c72591b00b7f08efa70ec438142f9fd22a89daa1f8981ed2dc2b10e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 6 9d531df8effbb51c64f08e99f1d5e2841eb4ef6c49d1397381a20a6576e16850 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 7 755e79e527c74cdce0d0b1b4dbea74d639ce939c306129e0ebea84cf7d72bd13 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 8 0d1b0b8b1729aa56df6c1bdcc08c9057c4fd2741b9a3d369242774f53944e02f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -8 4367ce7e43ce3dce053a5aa52b7e7b700a7e266239e91a70a90fc06b63d18ece 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -7 c59e78e77238284e754406e9e5edc9ab95836de31ea02b3f9d892a97b8cd78a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -6 c63548a3b36db9293dd8bca785b008a3e555cfc28ebd29c3f3f2db6b48ad7d90 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -5 4680afd2a8a2070863108cf1b6bf2d20526870039194856dbc01a247decf4433 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -4 697e060186a06cae5ceea0c2022ef734e8515834f154c339c4f858933af34bbb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -3 2819fced8d0234d52889df26a34816f77e5dcdcbd0ad6a84f14eb76805460ffb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -2 e07b5b1c0c9fa889833d12b7d4e0d77a6e13562115f7df0821d33d944ffb951c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-3 -1 fe3f09f13704393d2b7b1de01ad1e6019c2026732c7f37fb606e98b8caa8fc57 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 0 51f8a1a38a41616de5065389e8c2d8ab2ef140b84cbfdba5450455b5595b9d3d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 1 e581ea05cc318caeda834761bfd658a9a5c6a7538319a8054828ee9e130bf263 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 2 cd300f15b2ac4e29c99f9259f789f70693e187b47fb4327366d0a2f7e1b43767 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 3 9ca913943094b455fd9ddb2baf4a2d5d9804dd9d6306ed8fe9343f08c28f3699 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 4 22090840e16f339a5f2785d6499a431f00c39fce5d54f87b4cd338b03993729a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 5 62278637c9203e9dc118d6b572fa305988d3b8835e3c6f51317418e02807c569 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 6 fc79a0b5b1c3c5807de4a44f5859d4a9b0a71850ff423f2007f2690e15635eab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 7 a8369fe6fd76191aac527edcf568b15f6f222a96ded1fea44bf855f0ae9b48ae 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 8 55d37c3ee6dffcf24c0b831d0bbd8bf48391002bae36cb179d89d95ae7a21605 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -8 0a70e75164d5358712a0e87b6bf1a66c9bf42a43332eb2d1de9bd6661f0501e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -7 60e5e216ff3a4147a4a206861051a773924bf7d59910dc690bf78486ccf0a826 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -6 00fd0a1144b5a098953d57a1f2b43258c7796b5f023f072c15267e993258de85 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -5 cafd29e010f6af2ebe580b9b0bb710a54fd23592dade31b7e669b35c3f76f3fb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -4 622462ae5157344ddfd31919d4f829bd7de499d859e8ea9e7a78aece2948185c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -3 0298439e5b148523b1a4a39ef0ceb4d46cf05afa0409b0531b44e096ef3cc371 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -2 ba01546214f5c228ac0d49fd29af7e537e690e6c23ada25ff5ebb8b7cf8900a1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-2 -1 9ea6b0d37b3b9e81fe0d0384de53793b23075433b7c77bb7dd3b10dcea2ba0c1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 0 5b945f1828d88ac141f97bbe29587a2841d85a631c19e9575ee33f92d3340813 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 1 df696e4e814a2ede0ef1377cf1a1ab783577d4f328397ddebd8ad87cee12e535 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 2 dc6b7b03f4731400d9e04a20e0a3f6175ea7b5cf48a1dc9ec17d93b3b55d79f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 3 d9929169dba8ea107712fe27d739d662155b37ee61a54363f029365a89b110ae 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 4 8c6a4a674b635ba2cfd81b5eeb001c9feb27be24c4c63519f1b3bd5e5eedb27a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 5 bf7eda22dfb6b3b4200ffbdb769cd431556a60a6f475482775d2df4a36e6f348 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 6 0fb399a09635cb5b63def5091260c412e2471259f99b4cfa62fd9c636ecbf096 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 7 0b607c01a69c2265f51a6f8064fe74f1a79d92b8cfc4d1079f8d853de407b307 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 8 9cc17e392bb780aa2dbd777f2e639e23acaef1f27c5c60ad2d3d992b78d31d48 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -8 802a05904797cf3791f398056dbdcf9fa04cf8f30412f330e74a4c4344075558 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -7 85246d2e6d27b72a997a36b467b6938e0f8d0e9d1ba865bcc02f3391afb07e26 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -6 3b7485773bdcc52ad4dc09e2aa66aff5d2d339d481c7e8f4041f175486cf112f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -5 3de97a621cd4c0871e0bcad0bf260151ec13436496912e48d994b938a0b0e4d7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -4 6084ca4bd6718289312e4a510635608bdb9a53111f8f855f3f62436eb0eb5c19 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -3 7d2c092c140c2105a6e1be99c2535a97c240bcdac31a31ae724c0fd1af3d4415 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -2 040127151932013ebe1894f4204923cd18ad5f80b6b756d32e915d1261476e05 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
-1 -1 d534d548ba184029ad57ed1619cb66af64c8559d2e1916dbdc35352944a92002 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 0 9d634e5914634328e642e3ff4ee25b1d855f485f93af99e8ab3abfe396ad0711 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 1 642706de1286e15f8d63bd5b71c24dbe00906a4f0375e4c74fc65365aae72991 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 2 88a50a0c23489c5b7bc49181b2839ff5d9c0af0699101c778b2c6fcb4a0a8922 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 3 a6e306902412137b51c1f9e49b22874765a9fc474535e2b12fa6e98fcbfeb5df 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 4 b3200e2fcb52fd1e925d043eea822b06bf9628fadd1f58ad58c2656d33d1ae85 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 5 2b8861ca99726fa6cb1b85ccca44426c79b36fe61b9384e4bed568c5104feef2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 6 952eb848f973913ab7c037c4dead78872690720732d751fef6f4777315f6c44d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 7 b93001115c62d7ab2733a86b482b340fa5eb789ddb99039e7e299e14f1c106e4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 8 dad0064ff5ad856d2140e923b771ea92d0bdcf157e3bb813b79565ff9f473b36 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -8 e5b7f9b4885bbb15d3da42b3343610521e1b03a73bbf208b66b53833b180166b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -7 b6a2ff710e665cff92200cd77b21e575a3d7db02986aca3054a86d5f10b7f05c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -6 3ba1c1c28f3b875a40eacc39a74ef819bb52df10b3313c535edf5b973551e8f6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -5 905d0c485c49a9f01aec61d859b5548c4ec72eade81ca85c3864294d3677b151 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -4 33dc036e9420b269dcb5247f2df35e55a42310c6d8a6784ed121d56c7487ee00 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -3 2c8d81467deb96167f74cd6e1fc51ce97adeaf56596f0bf948f37edb6707bf0b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -2 64d93e292f86d8701d0ec2e7d099196b94d5fd179ceed471c28ae59e55ee28e2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
0 -1 708b7e4b5c912b10ed5147d2d87bd77b913a37fbde4155aac7ce6260b5471bcf 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 0 8f2cba2e6652796cbab43457666667d8ec5beef356095467fb75ba53e7de38ce 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 1 0b69855d8785d992be6743aba315c2b1014a58dab0b364af3394823652ca62ed 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 2 0db7977d44cbcbee49be1a963b42b60756f21077f04fabf78f3a71c617fc8db7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 3 c0c5bb35ddcbf2b25c360f100e949629c2533a326feae8d114fe465740eb08db 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 4 54822a01ccf10a646ed9459b82a12d177813bf4a23c55ea4f7aa92bf1afb9974 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 5 f01e402910c0d3760368d2a6486a2e6ecc9f63ca8e58765074fc920d25b0faed 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 6 2cbb7b541d8e8012c64fdc872270fc1400cc510182f4333d6a0b5dd21215d08e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 7 b711c83e157c349c2d3cfac9b6add13e097d507284f6fa62f18c24ae78afa814 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 8 76e802d6e2425307ca4644014b7b03e53fab0f4adc03dfa4baa7623d7d8b7413 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -8 56b4596f35a47200714e74589f9b89a8eb5a01df53f2e5035a294f4c80d88641 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -7 ae559e04a1367fe775479c35168382ee74bd450774c6bb785335d2ea468a741e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -6 e3e4f5ad931e99baecf24895bc7c3d7f8cfae5c3dc485c21ac47a3f5030e3617 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -5 047a5d16348cbac2e1add22d67268d5f1e43b7d03de0fe85edb6abea470c6cc9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -4 53021a6c0ca99c136c1dc7c7365c6a3b7b84dcae506fbf1ea54eb1cf6456dcc4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -3 83d897f90ec0fc2357c93bf07d43952df23c9d1d0d6d5b22ff73cac0ab338b7b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -2 afbb2abecfa34904954f2e16ffb56b49dd292170f90dd9552a2f101d101c01ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
1 -1 a22df312d77de965860d2bd1ff489d1c38b343c2beb11707949b444725c25a4c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 0 9abdce5ab4c5df4479bf095f5d200174650f4beb3504a99c26f988b3b7675898 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 1 a2cd93c410be4911fe0304da5d2993058ad5957f670df5e5d781e3a7fde37fbb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 2 19606f317ca9aae79e2d7f66c9ab14d9138a7b7d9bcc5d61a2040297fb1580a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 3 ae9e0cef6124a2fb1283cb349d698030f90b6f1509b46a88b79d4ad81c5075c3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 4 ceb1afc0561b22131484f37f704c66fb0be6e62262ddc778de791b3780b5b535 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 5 703003021b68774b61e040bb552f4883793828cdbe75dbcad424afe2220e18ab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 6 b7e2a7f81ffce5851a996898cacc38b3597426dd473ea6310dca7ffe126f99e3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 7 e13be39d90d9bd7e395e0763bebdf29e8c08d676ec78d5a16b7511710d302909 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 8 907fc6c166e0a65d37f5276072ccaa3fcab9064660836691e3f459c8a0cf42e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -8 86c8905772609245555d5860066b167773b4cb919a46a7ea7ec2fb8075399753 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -7 c132100840d3ac286482223957a18729768cff3f4cd0721c794771e4c50ec315 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -6 9a6becb1d8aa3c329623f53f1a1944e1e2a0f0988acb1d2cdb6c231afa211e90 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -5 cb8c69a2143e96e0c49c3131f209fee789513246f1afaf95cb01e3088ab299f4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -4 d256f0a962427bc0624eaf826516d981596fc252ad0d10d7204b1fc06477b07a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -3 11ddf89eb5ed8e7d5c6c0c98c209f1237260fdaa5f5ba1e9608f412a9b1d1a08 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -2 9bcc19afed624473a53a7d6492714d7fd36203c731c37fb2a0c483afcac0dd8f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
2 -1 44daf4251ccad570516d5c2490cfa8095da33dbd0f66465db700791db9642919 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 0 31a12ef94005ed541ecc4e83a2fa493f0d4d28a3ba08e9a5d9bfafb30aefe28d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 1 a5cb0ee02b85b4235cc64e5ae388ed0c0749712efe1ee79479b72555f05e4f30 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 2 9047f6e870831d21b794349be2c312a0df67022627e6288dcee60aa4159b5d22 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 3 f8888a1491bf3e2df54d40c3814ef2ea912b88ad40cbecdeb44f1fa5663a5f71 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 4 287401b60ebd6b4e4f7a73dbcd35c919b182fea0e62c41ddb1c1288537b62ca9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 5 dd885bb7003dd62933dab34145561b99f8035f8041c05ee6ef8346cd31b89933 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 6 8f8ede04cf803d45a24932177ba707688e09cc90d1b2cbabf6298a6b29d19eeb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 7 d825796e3a32cbbb3604aa65fdc417defb5effdc32e05696344d9a3c016ed474 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 8 260a4e2748902a3a2fb8d7af62929100ba6fe9975142286032f33fcfccf51f6c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -8 d66d74a3e4b2f247cdefeb554299b45cbabc8d879e22b95d4b27cfc8e5141cbe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -7 0bfea12ea04c7720f5648c89a01fd175be32e8a77cc4a7a603ed2876c62161dc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -6 626380d70931e53f62e83ae8118840eda96107e5f0965daeb199a2740e1c2e5b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -5 56b49e323ffc4ada99a906dfce0d1e2c11b31dc3e9052c0c468ed6420d31b858 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -4 f2350cb0d8dde09f298c613f5ad1d97fc382b0c1d5b12826eddb2f2195bc7831 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -3 8c57558a9a5644c05c5daa58d17e2be1b9de1a65d6f7deb4855297a97523f482 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -2 336127103c8f6aae0fd993713bd880199eeefb589442fa5ebc2386f0c022dce0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
3 -1 523a07c0a804ede754f78283488eddef94bfe25b72cacaf534263282dfd46be5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 0 cf509fb497c5d7bda76448d12a1a4fe8ce0fd38036a271fcd7b0c48449f82ab8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 1 59ab0838a0833b55c37406f68c447c2b084e88f4e17d59cd6dac4bfa30263b8a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 2 3588d7ae2ee246864fad05fd0f03dad8741ffcc0cb7704c11c1bd431b049b235 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 3 f70ae200940e87bc0df540e7921ccf5600b1c323b9c07368e07587f34c55d1c1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 4 6e98b393eaad8c62f1a2425559556100ca3dff5a13f82887e88093d9ef7d620a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 5 7d6899d1c1a8de24aae6e47ff09cf5f876e247b66f7925943b6bcc5c2fa18b2f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 6 4fd67412489af76fe1c5955685380e31a34475d401b3c7fcbb7c5d088faa42ad 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 7 774c770ffb3179e5b2c6d1fe97ff6ac8d3645aa725b52c4ccdd46260e8505ff5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 8 3097971d8936ed6d53a74ffb45f4eb98118bb7d364a34494de72a86f0b3cf2d5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -8 20ab184d0549d5312eb3c4c4c93c27d3dfaf3cf5dc599f35c291b928218938be 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -7 b22075a0dd7dd86db2e98414131576d3f28fec55d7e8ad623fdc92d2e5c77f17 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -6 d4e34b1219c0d25371cedb943dd3ff72fedf96e2d86d4d62e36c29195efbf1d4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -5 b13ceb0561b22bb5dc346608a63635892771509538f26a214d5cac8213207b99 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -4 40c1c9e7104508edae785a3534a98e9e74dd2a4f66c5853138fa441fb64f003e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -3 e4b49f322b501551da69e06e3ce41572ce6af154dffd1e4b27691c8153cd4fc4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -2 0fdb0c8ac97ac20a56c15058ae46a6e4c734ff6974dd85068005779f5685ea9e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
4 -1 35edd44c1c2f3001316a226dafdcd90aa67de5ae613ae56cad6d3ee985eac4e4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 0 b08c5b7e8d207372eb4be8e01289449e574f1d9e0f882d1af190529d23f3e001 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 1 282aa33dfda7d77a51448969f29f5414d452ab88e4ee7c531465746db049f394 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 2 2a6739e7840576314fbcf347982829160ca4a5fb910746cc7a7f93c457de360a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 3 ad2a08c773242a0f61c0d90a00c166b3de65466d09f02ce5daba95fa8af6ecfb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 4 8ed02630b5641e2efcce55ef70d0387898996d9fdc4979c41ca894a39776b365 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 5 d66c6279267ce407bbe5c0007a50bb48359d08cdf95cce6659be45963148e402 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 6 50bc4674208dde2e0f0124d95fbdeda90cd9d9aebfb584a0ca24680e9275bf46 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 7 e1080da9f31c8cf90e12335cacd75a3439f693f963e4e7da28643a4aeea62a56 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 8 fe11aca2e168ecdca408d4dcbc45b0083f834fee5de4ddbb35227f475925978a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -8 8f51ef501c2b4b16b808dc7cf3845ada51ea8edaa6f8681a234b592f27937983 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -7 69937c9e1b63da726e051f0725db5355e2bcb18376df2f3b3028cf804bb24db6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -6 c20e2b424f96d572cace671c9063268b764a08b9927bf4ca785dfbf300d4ec63 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -5 a14bedb28bcb68f69e39257ada66536a1eebd15af6aaf8dc9c868cc4808a079f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -4 044bf8274f32bad7bdc4a720611faac7ecf961d201790af3b14646cc6ef65280 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -3 f9e4bf03c9e9a0bb68e22aabffdcd10da64b91f736d1e09a7b43f4bc94a98077 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -2 fd6394bbe1b01be2f5fb8847f543e69bbbf6ed272c29650d721eb7a4de32b4e2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
5 -1 620d5b20d3cc817044d8e1087cf18cc42fce70ef04a26ff7b3ea6b824b685122 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 0 9f1d84e4b370154ed36eb76bd8feb1a92d4e0d04aed7e1fe55f30ba19248ae8a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 1 65501bdc9af1fa61b6ac7f3016d7db4529b9bf20f39866a569b6e58c73865297 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 2 c4258153706bd0863490040c8af3e32c6d39ce0e664e99069cf7b10f1a1ebebc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 3 43f539e2d1729422f8c7724f0bc2a0ad1bb750bbded45ee10dfb6f9cf8de9bfe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 4 329e0c6370d49ef08a3515a6491389a1dd1501a31ab98f9a7b64e87e2e43ca30 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 5 a980add3cfa56e95901b5d693cd741c97cf35e3fcdbde7272c48a39d3785575e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 6 3626dc0c6fb1e8f1c05573e6a309d97fa83cff6af9020948f5471fee24f11969 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 7 1ccdf04b99d41c2fdf5f62fa148f6c9a45285650a7fc0a8438d3bd80ec9a5753 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 8 12aaa847543f538ba7a85aa90edecaede9fa313e881b1dc560ee6e0e6640c8ff 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -8 683ce017cbc497d1b823090a0ec984c0478ddc4e3ec2077096b6569a54d0186b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -7 94ea7dea313d1ddb52b086acf681185b45de615ca19b07f9b31863ad50d63df5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -6 942e705c68ed7f1ef3c625843a06924dcb1299c10421bdaf3914c3cf091a2ddb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -5 7a8982f6e99e72e735d32f7685b486447642b80463b9ac09afa4c8d392d1cd8c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -4 22b2dac5870b720f01d68d38475287a34f0ed21f39dd9717b7dda9f96818ef1c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -3 20fa5ff56a7e160284fee9efe2731d505b59feac67ccacaf7c33f8cc92d2bece 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -2 7427be500743e898067c3879844931a0c1894cb62524717547267ed1e69eb4a0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
6 -1 98446f1c88d2efea8314f61f35f8fb01f0478c77fc5204095ba83fb53ac8c7ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 0 1196a06655cf67cc0d56ac2281315211a13769b9198bed293aa3813d088e9d44 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 1 2b50aedb8471f269114bb7e8be545c0066869bb7812d96c5019006d8bbfbc250 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 2 d71eea940fa42cf164940ccf32137f76bfbf78087e3bf55827b32c0539e3651a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 3 3b10ae5f226f8bfef16e84cafc24c3d06fcf7e9fe894024ce1bffd9548c2200b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 4 c627e0fdede69245d1d864e93f7d1fdb09bc28a8d1573de2d9bfcc2b3ef865a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 5 88ba2b1f11c70e7c4e132ef95d20e5fcddaad3aab614814b303a9af7e29e453a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 6 6bbc34b26da81a0820eb03b848f372f32fd9c09c41c5f4c094f4598b9b495a7d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 7 a7c48618de4cd7bf9cfbfc794514556f028755bc6dcce0a34cfae009d28ad4f1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 8 32885533d96c8ed9d11a23080df65d08c8360852ee79ea3923767808b06c599b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -8 3e7c6219d03bcb3d3f5e845735e648a9dc0ddeb4047f3004469e89058e954094 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -7 7202ef092e139a4f3119e32d827ce1fa0640d02329230a92c7e43f6893e8cfb7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -6 f57cac950053f8f2aa978e01473a91655bc32427befbf2f2eaaca5642304abbd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -5 e07049ac9a576241138f0c931bce0f737328b9f5cf5e2f076a33d0c4dc96843a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -4 48e72991ba4d7f30546a4db4e7d6e0eb3908ac2f45150d57aca666ace4e0ecbe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -3 7b2c25f93d898720159a89cdcf6fd64d108d3325a4b4b7136ad4054232f7a416 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -2 fda7e40529cf6ff9c3447d9fff9906c549f2998baba2c7edba22c0641156c3b7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
7 -1 88ae6027aca5f412c97a28bbb7c7d31dda5616a3f2600764f4ddf608f27ad1f4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 0 62b82563ed14f8182e3e4b83ff54cc41657cf55eec2a5112123cd7cf095f9995 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 1 02797a2b0765485eeea460b65f03a7a94937eb3300b5eb9d09fd1e09cee38894 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 2 c52de795f6ca08db1be71a2a4f518c8b6b70bdc53800d708503118ae95893f86 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 3 378b7f06bb7dace3bac1566f3148256a0d8365d2918926dc73186b40c25e1518 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 4 ddef83c202a59cb14b5b75bef83d678c11d1d0a33fa803f662c9f9cb435d3110 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 5 ab3866ec28e7eac2b18b12b539337cf3343816885e18a1498b7f7106288cc670 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 6 45e9ea668e339e9bccdb103eb6a055f10bb8dc49e7177a37b9a19a862f13e861 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 7 1871c550876139b2a533c43a3b6b6afbbdc4c3aa3aa903cca98e3b67f23cc4d2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 8 5a9a452a44f385b53ac52f5fc6921f2f9e865bb63b1f0958b2314f5526156ef8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -8 819d7f6becf5f5acf6aef8b750353ad867289aa8a8a6b3ced77e999c7958a47b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -7 57ac4798c2301dc72028f4704fbe897a2e871cc2d88957840a7f9264261cb6f1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -6 6da3519f7893f32772681a6e12f06edabb774e9e8d5a9293002608a8cc40e9fe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -5 2e2e6e327e0e27d6d6de0ea6618f219fd080f6f9de05b6d6602ebb90bad6a077 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -4 01989f7b6de2ec03426e2b68190d6fc75e2d57b24225305377338294ca2d14e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -3 e57520d1b8c17e9de905510adda0d94fe4d8309245db26ecfc6dedbdd7702719 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -2 35ba48632ddfcbe171d457105142dee5f1123f528f1a02c127377b80e83ca58f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
8 -1 ab693d879e0a8e294d19057eb9540fcf4d2daae5c4bd1b5df649b74dd86697a9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
|
||||
#combined=28b777380f2a5e467cb387f9c3fb08c8a3e6bdc6bc226c38ee6e14cc7e380da4
|
||||
@@ -1,75 +1,136 @@
|
||||
# Iris
|
||||
|
||||
The master branch is for the latest version of minecraft.
|
||||
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
|
||||
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
|
||||
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
|
||||
or NeoForge server mod, and produces bit-identical terrain on every platform for the same pack and
|
||||
seed. The master branch targets the current Minecraft version (26.2).
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
# Building
|
||||
Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
|
||||
development.
|
||||
## Platforms
|
||||
|
||||
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|
||||
| Platform | Artifact | Minecraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.2 | Full feature set |
|
||||
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.4+ |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2.0.12-beta+ |
|
||||
|
||||
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
|
||||
Java 25 is required on every platform.
|
||||
|
||||
### Command Line Builds
|
||||
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
|
||||
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
|
||||
workspaces with schema autocomplete over the server's live registries, entity spawning parity
|
||||
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
|
||||
the goldenhash determinism gate, which is interchangeable across all four platforms.
|
||||
|
||||
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
|
||||
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
||||
* Windows
|
||||
1. Start > Type `env` and press Enter
|
||||
2. Advanced > Environment Variables
|
||||
3. Under System Variables, click `New...`
|
||||
4. Variable Name: `JAVA_HOME`
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
|
||||
the example text)
|
||||
* MacOS
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 21
|
||||
2. Run `sudo nano ~/.zshenv`
|
||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||
3. Once the project has setup, run `gradlew iris`
|
||||
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
## Install
|
||||
|
||||
### IDE Builds (for development)
|
||||
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
|
||||
the server. On first boot Iris downloads the default `overworld` pack automatically.
|
||||
|
||||
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
|
||||
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris
|
||||
downloads the default `overworld` pack before the worldgen datapack is written, so the default
|
||||
pack is fully active immediately. Packs installed later register their custom dimension types
|
||||
(height ranges) and custom biomes through the forced datapack at server start — restart once after
|
||||
adding a pack so worlds get its full heights and biomes; worlds created before that restart run
|
||||
with fallback heights.
|
||||
|
||||
# Iris Toolbelt
|
||||
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
|
||||
Create New World screen; the integrated server runs the same engine.
|
||||
|
||||
Everyone needs a tool-belt.
|
||||
## The client mod
|
||||
|
||||
```java
|
||||
package com.volmit.iris.core.tools;
|
||||
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
|
||||
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
|
||||
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
|
||||
|
||||
// Get IrisDataManager from a world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getData();
|
||||
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
|
||||
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
|
||||
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
|
||||
mod is inert.
|
||||
|
||||
// Get Default Engine from world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
|
||||
## Quickstart
|
||||
|
||||
// Get the engine at the given height
|
||||
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
|
||||
Create and enter an Iris world.
|
||||
|
||||
// IS THIS THING ON?
|
||||
boolean yes=IrisToolbelt.isIrisWorld(world);
|
||||
Plugin (optional arguments are keyed):
|
||||
|
||||
// GTFO for worlds (moves players to any other world, just not this one)
|
||||
IrisToolbelt.evacuate(world);
|
||||
|
||||
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
||||
.name("myWorld") // The world name
|
||||
.dimension("terrifyinghands")
|
||||
.seed(69133742) // The world seed
|
||||
.pregen(PregenTask // Define a pregen job to run
|
||||
.builder()
|
||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
|
||||
.build())
|
||||
.create();
|
||||
```
|
||||
/iris create myworld type=overworld seed=1337
|
||||
/iris load myworld
|
||||
```
|
||||
|
||||
Mod (positional arguments):
|
||||
|
||||
```
|
||||
/iris create myworld overworld 1337
|
||||
```
|
||||
|
||||
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
|
||||
servers they are positional and composable:
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
|
||||
```
|
||||
|
||||
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
|
||||
console/status output. `/iris pregen status` reports progress on the plugin.
|
||||
|
||||
## Studio and VSCode workspace
|
||||
|
||||
The studio is the pack authoring environment, available on all platforms. Studio worlds are
|
||||
transient — they are deleted on close and purged at startup.
|
||||
|
||||
```
|
||||
/iris studio create <name> [template] scaffold a new pack (default template: example)
|
||||
/iris studio open <pack> [seed] open a temporary studio world for live editing
|
||||
/iris studio vscode [pack] write a .code-workspace with JSON schemas
|
||||
/iris studio update [pack] regenerate the workspace schemas
|
||||
/iris studio close close and discard the studio world
|
||||
```
|
||||
|
||||
As in the quickstart, optional arguments are keyed on the plugin (`seed=1234`) and positional on
|
||||
the mod.
|
||||
|
||||
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
|
||||
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
|
||||
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
|
||||
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
|
||||
files hotloads the changes and regenerates the schemas.
|
||||
|
||||
## Building from source
|
||||
|
||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||
|
||||
```
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
builds every platform artifact into `dist/`:
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
Iris v<version> [Fabric] <mc>+<loader>.jar
|
||||
Iris v<version> [Forge] <mc>+<loader>.jar
|
||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
||||
```
|
||||
|
||||
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
|
||||
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
|
||||
Discord asking for free copies or a compile tutorial.
|
||||
|
||||
## Maintainer docs
|
||||
|
||||
- [Minecraft version bump checklist](docs/mc-version-bump.md)
|
||||
- [Release checklist](docs/release-checklist.md)
|
||||
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
private static final int NOISE_BIOME_CACHE_MAX = 262144;
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
this.seed = seed;
|
||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
|
||||
}
|
||||
biomes.add(vanillaHolder);
|
||||
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder == null) {
|
||||
throw new IllegalStateException("Iris custom structure biome '"
|
||||
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
|
||||
}
|
||||
biomes.add(customHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (biomes.isEmpty()) {
|
||||
throw new IllegalStateException("Iris pack '" + engine.getName()
|
||||
+ "' has no registered structure biomes");
|
||||
}
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
return invokeFor(type, source);
|
||||
}
|
||||
|
||||
private static Object fieldFor(Class<?> returns, Object in) {
|
||||
return fieldForClass(returns, in.getClass(), in);
|
||||
}
|
||||
|
||||
private static Object invokeFor(Class<?> returns, Object in) {
|
||||
for (Method i : in.getClass().getMethods()) {
|
||||
if (i.getReturnType().equals(returns)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
|
||||
return i.invoke(in);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
|
||||
for (Field i : sourceType.getDeclaredFields()) {
|
||||
if (i.getType().equals(returnType)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
|
||||
return (T) i.get(in);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return possibleStructureBiomes().stream();
|
||||
}
|
||||
|
||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||
ensureCachesCurrent();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
||||
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
KMap<String, Holder<Biome>> m = new KMap<>();
|
||||
if (customRegistry == null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(j.getId(), fallback);
|
||||
}
|
||||
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
m.put(j.getId(), holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
|
||||
if (customRegistry == null || registry == null) {
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||
ensureCachesCurrent();
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
return vanillaSpawnBiomes.get(biome.value());
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
boolean monumentQuery = radius == 29
|
||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||
return false;
|
||||
}
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = (quartY << 2) - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
return internalY > caveSwitchInternalY;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no surface structure biome at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
noiseBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
biomeCustomRegistry, engine, fallbackBiome);
|
||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine);
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveBiomeHolder(
|
||||
biomeRegistry, resolution.irisBiome.getStructureDerivativeKey());
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block "
|
||||
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (deepUnderground) {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
if (fallbackBiome != null) {
|
||||
return fallbackBiome;
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static long packColumnKey(int x, int z) {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
||||
);
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
|
||||
if (optionalReferenceHolder.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return optionalReferenceHolder.get();
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
|
||||
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(biomeKey);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
|
||||
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
|
||||
Holder<Biome> vanilla = firstHolder(registry);
|
||||
if (vanilla != null) {
|
||||
return vanilla;
|
||||
}
|
||||
|
||||
return firstHolder(customRegistry);
|
||||
}
|
||||
|
||||
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Biome biome : registry) {
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
|
||||
if (optionalHolder.isPresent()) {
|
||||
return optionalHolder.get();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
}
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private final int runtimeMinY;
|
||||
private final int runtimeHeight;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
}
|
||||
|
||||
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator());
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
this.runtimeMinY = level.getMinY();
|
||||
this.runtimeHeight = level.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), findUnexplored);
|
||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
|
||||
? null
|
||||
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||
}
|
||||
|
||||
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||
HolderSet<Structure> holders,
|
||||
BlockPos pos, int radius,
|
||||
boolean findUnexplored) {
|
||||
if (findUnexplored) {
|
||||
return null;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
BlockPos best = null;
|
||||
Holder<Structure> bestHolder = null;
|
||||
long bestDist = Long.MAX_VALUE;
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
|
||||
}
|
||||
String structureId = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
engine, structureId, pos.getX(), pos.getZ(), radius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
throw new IllegalStateException("Iris structure locate reached its safety limit for "
|
||||
+ structureId + " within " + radius + " chunks");
|
||||
}
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) result.originX() - pos.getX();
|
||||
long dz = (long) result.originZ() - pos.getZ();
|
||||
long d = dx * dx + dz * dz;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
return best == null ? null : Pair.of(best, bestHolder);
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<NativeLocateCandidate> candidates = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
|
||||
}
|
||||
String key = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (!decision.generate()) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(new NativeLocateCandidate(holder, key));
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return HolderSet.direct(List.of());
|
||||
}
|
||||
Set<String> reachable = reachableStructureKeys(level);
|
||||
List<Holder<Structure>> kept = new ArrayList<>(candidates.size());
|
||||
for (NativeLocateCandidate candidate : candidates) {
|
||||
if (reachable.contains(candidate.key())) {
|
||||
kept.add(candidate.holder());
|
||||
}
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
}
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
private Set<String> reachableStructureKeys(ServerLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
ReachableStructureCache cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
Set<String> reachable = Set.copyOf(
|
||||
VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
|
||||
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDelegate() {
|
||||
if (delegate instanceof CustomChunkGenerator chunkGenerator)
|
||||
return chunkGenerator.getDelegate();
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return runtimeMinY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return runtimeMinY + engine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
ChunkPos chunkPos = access.getPos();
|
||||
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
|
||||
IrisNativeStructureDecision decision;
|
||||
try {
|
||||
decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
structureId, undergroundStep);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
if (!decision.generate()) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start,
|
||||
structureId,
|
||||
decision.yShift(),
|
||||
getSeaLevel(),
|
||||
access.getMinY(),
|
||||
access.getMinY() + access.getHeight(),
|
||||
undergroundStep,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
|
||||
return delegate.createState(holderlookup, randomstate, i, conf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
||||
return CompletableFuture.completedFuture(ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = delegate.getMobsAt(
|
||||
vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition);
|
||||
if (resolvedSpawns != vanillaSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) {
|
||||
applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> list, RandomState randomstate, BlockPos blockposition) {
|
||||
delegate.addDebugScreenInfo(list, randomstate, blockposition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
ChunkPos disabledChunk = chunk.getPos();
|
||||
throw new IllegalStateException("Iris cannot generate native structures in chunk "
|
||||
+ disabledChunk.x() + "," + disabledChunk.z()
|
||||
+ " because structure generation is disabled outside the pack; enable native structure generation "
|
||||
+ "and deny individual structures through importedStructures.disabled");
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<List<Structure>> byStep = structuresByStep(registry);
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<StructureStart> nativeStarts = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
Object id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
try {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate()) {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
nativeStarts.addAll(resolvedStarts);
|
||||
boolean clearEntireFootprint = NativeStructurePostProcessor
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareSurfaceStructures(
|
||||
world, area, nativeStarts,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (StructureStart start : group.starts()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
|
||||
if (placementGroups.isEmpty()) {
|
||||
return "<no resolved native structures>";
|
||||
}
|
||||
StringBuilder context = new StringBuilder("[");
|
||||
for (int i = 0; i < placementGroups.size(); i++) {
|
||||
if (i > 0) {
|
||||
context.append(", ");
|
||||
}
|
||||
context.append(placementGroups.get(i).structureId());
|
||||
}
|
||||
return context.append(']').toString();
|
||||
}
|
||||
|
||||
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
|
||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||
IrisNativeStructureDecision decision) {
|
||||
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolveStiltBlock,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<List<Structure>> grouped = new ArrayList<>(steps);
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.add(new ArrayList<>());
|
||||
}
|
||||
for (Structure structure : registry) {
|
||||
grouped.get(structure.step().ordinal()).add(structure);
|
||||
}
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
|
||||
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Bukkit block at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
if (blockData instanceof IrisCustomData customData) {
|
||||
blockData = customData.getBase();
|
||||
}
|
||||
if (blockData instanceof CraftBlockData craftBlockData) {
|
||||
return craftBlockData.getState();
|
||||
}
|
||||
throw new IllegalStateException("Configured native structure stilt palette resolved unsupported Bukkit block data "
|
||||
+ blockData.getClass().getName() + " at " + x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = minY + chunk.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
|
||||
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int wX = x + blockPos.getX();
|
||||
int wZ = z + blockPos.getZ();
|
||||
|
||||
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
|
||||
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
|
||||
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
||||
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
||||
}
|
||||
}
|
||||
|
||||
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
|
||||
delegate.spawnOriginalMobs(regionlimitedworldaccess);
|
||||
}
|
||||
|
||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
|
||||
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
|
||||
Set<EntityType<?>> explicitTypes = new HashSet<>();
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
|
||||
explicitTypes.add(entry.value().type());
|
||||
}
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
|
||||
if (!explicitTypes.contains(entry.value().type())) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
entries.addAll(explicitSpawns.unwrap());
|
||||
return WeightedList.of(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
|
||||
return delegate.getSpawnHeight(levelheightaccessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return runtimeHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
int block = engine.getHeight(i, j, true);
|
||||
int water = engine.getHeight(i, j, false);
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
for (int k = 0; k < column.length; k++) {
|
||||
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
|
||||
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
|
||||
else column[k] = Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Identifier> getTypeNameForDataFixer() {
|
||||
return delegate.getTypeNameForDataFixer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
delegate.validate();
|
||||
}
|
||||
|
||||
static {
|
||||
Field biomeSource = null;
|
||||
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
|
||||
if (!field.getType().equals(BiomeSource.class))
|
||||
continue;
|
||||
biomeSource = field;
|
||||
break;
|
||||
}
|
||||
if (biomeSource == null)
|
||||
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
|
||||
|
||||
Method setHeight = null;
|
||||
for (Method method : Heightmap.class.getDeclaredMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|
||||
|| !method.getReturnType().equals(void.class))
|
||||
continue;
|
||||
setHeight = method;
|
||||
break;
|
||||
}
|
||||
if (setHeight == null)
|
||||
throw new RuntimeException("Could not find setHeight method in Heightmap!");
|
||||
|
||||
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
|
||||
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
|
||||
}
|
||||
|
||||
private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) {
|
||||
try {
|
||||
BIOME_SOURCE.set(generator, source);
|
||||
if (generator instanceof CustomChunkGenerator custom)
|
||||
BIOME_SOURCE.set(custom.getDelegate(), source);
|
||||
|
||||
return generator;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
|
||||
int featureIndex, int step, List<StructureStart> starts) {
|
||||
}
|
||||
|
||||
private record NativeLocateCandidate(Holder<Structure> holder, String key) {
|
||||
}
|
||||
}
|
||||
+1388
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class VanillaStructureBiomes {
|
||||
private VanillaStructureBiomes() {
|
||||
}
|
||||
|
||||
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
if (source == null) {
|
||||
throw new IllegalStateException("Minecraft chunk generator has no biome source");
|
||||
}
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
|
||||
? customBiomeSource.possibleStructureBiomes()
|
||||
: source.possibleBiomes();
|
||||
for (Holder<Biome> holder : possibleBiomes) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
if (keys.isEmpty()) {
|
||||
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||
if (access == null) {
|
||||
throw new IllegalStateException("Minecraft registry access is unavailable");
|
||||
}
|
||||
if (structureKey == null || structureKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Registered structure key must not be blank");
|
||||
}
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||
if (structure == null) {
|
||||
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
if (keys.isEmpty()) {
|
||||
throw new IllegalStateException("Registered structure '" + structureKey
|
||||
+ "' exposes no registered biome keys");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Minecraft server level is unavailable");
|
||||
}
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
Set<String> possible = possibleBiomeKeys(source);
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
||||
for (Holder<Biome> holder : entry.getValue().biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
|
||||
reachable.add(entry.getKey().identifier().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CustomBiomeSourceStructureContractTest {
|
||||
@Test
|
||||
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
|
||||
assertTrue(source.contains("resolveBiomeHolder(registry, i.getStructureDerivativeKey())"));
|
||||
assertTrue(source.contains("resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey())"));
|
||||
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
|
||||
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisChunkGeneratorFailureContractTest {
|
||||
@Test
|
||||
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
|
||||
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
|
||||
String locate = source.substring(locateStart, locateEnd);
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
|
||||
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
|
||||
String reachability = source.substring(reachabilityStart, reachabilityEnd);
|
||||
|
||||
assertTrue(locate.contains("reached its safety limit"));
|
||||
assertTrue(locate.contains("unregistered structure holder"));
|
||||
assertFalse(locate.contains("catch (Throwable"));
|
||||
assertFalse(locate.contains("IrisLogging.reportError"));
|
||||
assertFalse(reachability.contains("catch (Throwable"));
|
||||
assertFalse(reachability.contains("reachable = Set.of()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
|
||||
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
|
||||
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
|
||||
assertTrue(adjustment.contains("\"vertical adjustment\""));
|
||||
assertFalse(adjustment.contains("IrisLogging.reportError"));
|
||||
assertTrue(placement.contains("\"resolution\""));
|
||||
assertTrue(placement.contains("\"terrain integration\""));
|
||||
assertTrue(placement.contains("\"vegetation cleanup\""));
|
||||
assertTrue(placement.contains("\"placement\""));
|
||||
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
|
||||
assertTrue(placement.contains("prepareSurfaceStructures"));
|
||||
assertTrue(placement.contains("clearIntersectingVegetation"));
|
||||
assertTrue(placement.indexOf("prepareSurfaceStructures")
|
||||
< placement.indexOf("clearIntersectingVegetation"));
|
||||
assertTrue(placement.indexOf("clearIntersectingVegetation")
|
||||
< placement.indexOf("for (NativePlacementGroup group"));
|
||||
assertFalse(placement.contains("IrisLogging.reportError"));
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void nativeLocateAllowsMonumentsAfterPolicyAndReachabilityChecks() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
|
||||
assertTrue(findStart >= 0);
|
||||
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
|
||||
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
|
||||
assertTrue(irisHelperStart > findStart);
|
||||
assertTrue(filterStart > irisHelperStart);
|
||||
String outerMethod = source.substring(findStart, irisHelperStart);
|
||||
String irisHelper = source.substring(irisHelperStart, filterStart);
|
||||
int policyResolution = irisHelper.indexOf("NativeStructureGenerationPolicy.resolve(engine,");
|
||||
int unexploredGuard = irisHelper.indexOf("if (findUnexplored)");
|
||||
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
|
||||
int replacementCheck = irisHelper.indexOf(
|
||||
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS");
|
||||
int irisLocate = irisHelper.indexOf("IrisStructureLocator.locate(", replacementCheck);
|
||||
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
|
||||
int limitSkip = irisHelper.indexOf("continue;", searchLimit);
|
||||
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
|
||||
int delegateLocate = outerMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
|
||||
int nearestSelection = outerMethod.indexOf(
|
||||
"NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated)");
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
|
||||
assertTrue(reachabilityStart > filterStart);
|
||||
String filterMethod = source.substring(filterStart, reachabilityStart);
|
||||
int policyFilter = filterMethod.indexOf("if (!decision.generate())");
|
||||
int filterContinue = filterMethod.indexOf("continue;", policyFilter);
|
||||
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue);
|
||||
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
|
||||
|
||||
assertTrue(policyResolution >= 0);
|
||||
assertTrue(unexploredGuard >= 0);
|
||||
assertTrue(registryLookup > unexploredGuard);
|
||||
assertTrue(policyResolution > registryLookup);
|
||||
assertTrue(replacementCheck > policyResolution);
|
||||
assertTrue(irisLocate > replacementCheck);
|
||||
assertTrue(searchLimit > irisLocate);
|
||||
assertTrue(limitSkip > searchLimit);
|
||||
assertTrue(nativeFilter >= 0);
|
||||
assertTrue(delegateLocate > nativeFilter);
|
||||
assertTrue(nearestSelection > delegateLocate);
|
||||
assertTrue(policyFilter >= 0);
|
||||
assertTrue(filterContinue > policyFilter);
|
||||
assertTrue(emptyNativePartition > filterContinue);
|
||||
assertTrue(reachabilityLookup > emptyNativePartition);
|
||||
assertFalse(filterMethod.contains("NativeStructureLocateCapability"));
|
||||
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
|
||||
assertTrue(irisHelper.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||
|
||||
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
|
||||
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
|
||||
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
|
||||
int terrainFloor = source.indexOf("y > terrainY", stiltPlacement);
|
||||
|
||||
assertTrue(placement >= 0);
|
||||
assertTrue(stiltPlacement > placement);
|
||||
assertTrue(occupancyCheck > stiltPlacement);
|
||||
assertTrue(terrainFloor > stiltPlacement);
|
||||
assertFalse(source.contains("state.equals("));
|
||||
assertFalse(source.contains("snapshot.states"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
int placementStart = source.indexOf("public static int applyVerticalPlacement");
|
||||
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
|
||||
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
|
||||
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
|
||||
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
|
||||
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
|
||||
|
||||
assertTrue(placementStart >= 0);
|
||||
assertTrue(shiftStart > placementStart);
|
||||
assertTrue(alignmentStart > shiftStart);
|
||||
assertTrue(moveStart > alignmentStart);
|
||||
assertTrue(movePieceStart > moveStart);
|
||||
assertTrue(monumentChildrenStart > movePieceStart);
|
||||
String placementMethod = source.substring(placementStart, shiftStart);
|
||||
String shiftMethod = source.substring(shiftStart, alignmentStart);
|
||||
String moveMethod = source.substring(moveStart, movePieceStart);
|
||||
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
|
||||
|
||||
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
|
||||
assertTrue(placementMethod.contains("return applyVerticalShift("));
|
||||
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
|
||||
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
|
||||
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
|
||||
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingPlacementFailureContractTest {
|
||||
@Test
|
||||
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
|
||||
int methodStart = source.indexOf("public int[] placeStructure(");
|
||||
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int catchStart = method.indexOf("catch (RuntimeException e)");
|
||||
|
||||
assertTrue(catchStart >= 0);
|
||||
String failurePath = method.substring(catchStart);
|
||||
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
|
||||
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
|
||||
assertFalse(failurePath.contains("return null;"));
|
||||
assertFalse(failurePath.contains("catch (Throwable"));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingStructureFailureContractTest {
|
||||
@Test
|
||||
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
|
||||
List<String> methodNames = List.of(
|
||||
"getStructureKeys",
|
||||
"getStructureSetKeys",
|
||||
"getReachableStructureKeys",
|
||||
"getStructureBiomeKeys",
|
||||
"getPossibleBiomeKeys");
|
||||
|
||||
for (String methodName : methodNames) {
|
||||
String method = methodSource(source, methodName);
|
||||
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
|
||||
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
|
||||
assertFalse(methodName + " must not log and return an empty registry result",
|
||||
method.contains("IrisLogging.reportError"));
|
||||
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String methodSource(String source, String methodName) {
|
||||
int start = source.indexOf("public KList<String> " + methodName + "(");
|
||||
int end = source.indexOf("\n @Override", start + 1);
|
||||
if (start < 0 || end < 0) {
|
||||
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
|
||||
}
|
||||
return source.substring(start, end);
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorMonumentTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 63, -64, 320, false, (x, z) -> 0);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(39, start.getBoundingBox().minY());
|
||||
assertEquals(61, start.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
|
||||
List<StructurePiece> children = NativeStructurePostProcessor.monumentChildPieces(building);
|
||||
int[] childMinY = minimumYs(children);
|
||||
BoundingBox cachedBounds = start.getBoundingBox();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
|
||||
assertEquals(-13, offset);
|
||||
assertSame(cachedBounds, start.getBoundingBox());
|
||||
assertEquals(26, building.getBoundingBox().minY());
|
||||
assertEquals(48, building.getBoundingBox().maxY());
|
||||
assertEquals(26, start.getBoundingBox().minY());
|
||||
assertEquals(48, start.getBoundingBox().maxY());
|
||||
assertTrue(children.size() > 20);
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
|
||||
}
|
||||
|
||||
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
assertEquals(0, repeatedOffset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 3, 50, -256, 512, false, (x, z) -> 0);
|
||||
|
||||
assertEquals(-10, offset);
|
||||
assertEquals(29, start.getBoundingBox().minY());
|
||||
assertEquals(51, start.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
|
||||
long seed = 1337L;
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
StructureStart initial = monumentStart(seed);
|
||||
NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
initial, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
|
||||
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||
StructureStart reloaded = new StructureStart(
|
||||
monumentStructure(), chunkPos, 0, regenerated);
|
||||
|
||||
assertEquals(39, reloaded.getBoundingBox().minY());
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
reloaded, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
assertEquals(-13, offset);
|
||||
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
try {
|
||||
NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, -50, -64, 320, false, (x, z) -> 0);
|
||||
} catch (IllegalStateException error) {
|
||||
assertTrue(error.getMessage().contains("cannot align"));
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
|
||||
}
|
||||
|
||||
private static StructureStart monumentStart(long seed) {
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
|
||||
RandomSource.create(seed), -29, -29, Direction.NORTH);
|
||||
return new StructureStart(
|
||||
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
|
||||
}
|
||||
|
||||
private static OceanMonumentStructure monumentStructure() {
|
||||
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
}
|
||||
|
||||
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
|
||||
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
|
||||
}
|
||||
|
||||
private static int[] minimumYs(List<StructurePiece> pieces) {
|
||||
int[] ys = new int[pieces.size()];
|
||||
for (int i = 0; i < pieces.size(); i++) {
|
||||
ys[i] = pieces.get(i).getBoundingBox().minY();
|
||||
}
|
||||
return ys;
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlySurfaceBeardThinStructuresPrepareTerrain() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundAdjustmentsNeverPrepareSurfaceTerrain() {
|
||||
for (TerrainAdjustment adjustment : List.of(
|
||||
TerrainAdjustment.BEARD_THIN,
|
||||
TerrainAdjustment.BURY,
|
||||
TerrainAdjustment.BEARD_BOX,
|
||||
TerrainAdjustment.ENCAPSULATE)) {
|
||||
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
|
||||
adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
|
||||
NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2);
|
||||
|
||||
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(anchor), 2, 2, 64));
|
||||
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(anchor), 16, 2, 64));
|
||||
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(anchor), 17, 2, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceAnchorRaisesAndLowersThroughTheTaper() {
|
||||
NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2);
|
||||
NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2);
|
||||
|
||||
assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(raised), 10, 2, 64));
|
||||
assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(lowered), 10, 2, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void containingRigidFloorsHaveDeterministicPriority() {
|
||||
NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2);
|
||||
NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1);
|
||||
|
||||
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(rigid, junction), 2, 2, 64));
|
||||
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(junction, rigid), 2, 2, 64));
|
||||
|
||||
NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1);
|
||||
NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2);
|
||||
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(weakTie, strongTie), 2, 2, 64));
|
||||
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(strongTie, weakTie), 2, 2, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void containingFootprintOverridesAnAdjacentPiecesFalloff() {
|
||||
NativeStructurePostProcessor.SurfaceAnchor local =
|
||||
new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2);
|
||||
NativeStructurePostProcessor.SurfaceAnchor adjacent =
|
||||
new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2);
|
||||
|
||||
assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(adjacent), 4, 2, 64));
|
||||
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(local, adjacent), 4, 2, 64));
|
||||
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(adjacent, local), 4, 2, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() {
|
||||
NativeStructurePostProcessor.SurfaceAnchor high =
|
||||
new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2);
|
||||
NativeStructurePostProcessor.SurfaceAnchor low =
|
||||
new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2);
|
||||
int previous = NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(high, low), 0, 0, 64);
|
||||
|
||||
for (int x = 1; x <= 12; x++) {
|
||||
int forward = NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(high, low), x, 0, 64);
|
||||
int reversed = NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(low, high), x, 0, 64);
|
||||
assertEquals(forward, reversed);
|
||||
assertTrue(Math.abs(forward - previous) <= 4);
|
||||
previous = forward;
|
||||
}
|
||||
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
|
||||
List.of(high, low), 6, 0, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceColumnsMutateTerrainAndRemoveUnsupportedDecoration() {
|
||||
Map<BlockPos, BlockState> lowered = new HashMap<>();
|
||||
put(lowered, 0, 61, 0, Blocks.STONE.defaultBlockState());
|
||||
put(lowered, 0, 62, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(lowered, 0, 63, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.applySurfaceColumn(
|
||||
world(lowered), new BlockPos.MutableBlockPos(),
|
||||
0, 0, 64, 62, -64, 319);
|
||||
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), state(lowered, 0, 61, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(lowered, 0, 62, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 63, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 64, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 65, 0));
|
||||
|
||||
Map<BlockPos, BlockState> raised = new HashMap<>();
|
||||
put(raised, 0, 63, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.applySurfaceColumn(
|
||||
world(raised), new BlockPos.MutableBlockPos(),
|
||||
0, 0, 64, 68, -64, 319);
|
||||
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 65, 0));
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 66, 0));
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 67, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void raisedSurfaceTerrainDoesNotSliceTreeBlocks() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockState log = Blocks.OAK_LOG.defaultBlockState();
|
||||
BlockState leaves = Blocks.OAK_LEAVES.defaultBlockState();
|
||||
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
put(blocks, 0, 66, 0, log);
|
||||
put(blocks, 0, 68, 0, leaves);
|
||||
|
||||
NativeStructurePostProcessor.applySurfaceColumn(
|
||||
world(blocks), new BlockPos.MutableBlockPos(),
|
||||
0, 0, 64, 68, -64, 319);
|
||||
|
||||
assertEquals(log, state(blocks, 0, 66, 0));
|
||||
assertEquals(leaves, state(blocks, 0, 68, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweredFluidColumnsRemainFluidFilled() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState());
|
||||
put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.applySurfaceColumn(
|
||||
world(blocks), new BlockPos.MutableBlockPos(),
|
||||
0, 0, 64, 62, -64, 319);
|
||||
|
||||
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 63, 0));
|
||||
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 64, 0));
|
||||
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 65, 0));
|
||||
assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
|
||||
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
|
||||
|
||||
assertEquals(SinglePoolElement.class, field.getDeclaringClass());
|
||||
assertEquals(Either.class, field.getType());
|
||||
assertTrue(Modifier.isProtected(field.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(field.getModifiers()));
|
||||
assertFalse(Modifier.isStatic(field.getModifiers()));
|
||||
assertTrue(field.trySetAccessible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeTemplatesAndLegacyAirUseTheExactContract() {
|
||||
StructureTemplate runtimeTemplate = new StructureTemplate();
|
||||
|
||||
assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference(
|
||||
Either.right(runtimeTemplate), null));
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false));
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatedTemplateAirClearsOnlyInsideTheChunkAndAboveTheFloor() throws Exception {
|
||||
StructureTemplate.StructureBlockInfo clear = new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(1, 0, 0), Blocks.AIR.defaultBlockState(), null);
|
||||
StructureTemplate.StructureBlockInfo belowFloor = new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, -1, 0), Blocks.AIR.defaultBlockState(), null);
|
||||
StructureTemplate.StructureBlockInfo outsideChunk = new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(3, 0, 0), Blocks.AIR.defaultBlockState(), null);
|
||||
StructureTemplate template = template(List.of(clear, belowFloor, outsideChunk));
|
||||
BlockPos origin = new BlockPos(10, 80, 10);
|
||||
StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.CLOCKWISE_90);
|
||||
BlockPos clearPosition = origin.offset(StructureTemplate.calculateRelativePosition(settings, clear.pos()));
|
||||
BlockPos belowFloorPosition = origin.offset(
|
||||
StructureTemplate.calculateRelativePosition(settings, belowFloor.pos()));
|
||||
BlockPos outsidePosition = origin.offset(
|
||||
StructureTemplate.calculateRelativePosition(settings, outsideChunk.pos()));
|
||||
BoundingBox area = new BoundingBox(
|
||||
Math.min(clearPosition.getX(), belowFloorPosition.getX()),
|
||||
Math.min(clearPosition.getY(), belowFloorPosition.getY()),
|
||||
Math.min(clearPosition.getZ(), belowFloorPosition.getZ()),
|
||||
Math.max(clearPosition.getX(), belowFloorPosition.getX()),
|
||||
Math.max(clearPosition.getY(), belowFloorPosition.getY()),
|
||||
Math.max(clearPosition.getZ(), belowFloorPosition.getZ()));
|
||||
settings.setBoundingBox(area);
|
||||
assertFalse(area.isInside(outsidePosition));
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
blocks.put(clearPosition, Blocks.DIRT.defaultBlockState());
|
||||
blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState());
|
||||
blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.clearTemplateAir(
|
||||
world(blocks), template, origin, 80, settings);
|
||||
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition));
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(belowFloorPosition));
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(outsidePosition));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() {
|
||||
BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15);
|
||||
|
||||
assertTrue(NativeStructurePostProcessor.intersects(
|
||||
new BoundingBox(15, 60, 15, 30, 90, 30), area));
|
||||
assertFalse(NativeStructurePostProcessor.intersects(
|
||||
new BoundingBox(16, 60, 16, 30, 90, 30), area));
|
||||
assertFalse(NativeStructurePostProcessor.intersects(
|
||||
new BoundingBox(0, 320, 0, 15, 350, 15), area));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateAirDoesNotEraseTreesInsideVillagePieces() throws Exception {
|
||||
BlockPos origin = new BlockPos(0, 80, 0);
|
||||
StructureTemplate template = template(List.of(
|
||||
new StructureTemplate.StructureBlockInfo(BlockPos.ZERO, Blocks.AIR.defaultBlockState(), null)));
|
||||
StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.NONE);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockState log = Blocks.OAK_LOG.defaultBlockState();
|
||||
blocks.put(origin, log);
|
||||
|
||||
NativeStructurePostProcessor.clearTemplateAir(world(blocks), template, origin, 80, settings);
|
||||
|
||||
assertEquals(log, blocks.get(origin));
|
||||
}
|
||||
|
||||
private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) {
|
||||
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
|
||||
}
|
||||
|
||||
private static StructureTemplate template(
|
||||
List<StructureTemplate.StructureBlockInfo> blocks) throws Exception {
|
||||
Constructor<StructureTemplate.Palette> constructor =
|
||||
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
|
||||
assertTrue(constructor.trySetAccessible());
|
||||
StructureTemplate.Palette palette = constructor.newInstance(blocks);
|
||||
StructureTemplate template = new StructureTemplate();
|
||||
template.palettes.add(palette);
|
||||
return template;
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("getBlockState")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
return state(blocks, position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
if (methodName.equals("setBlock")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
BlockState blockState = (BlockState) arguments[1];
|
||||
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
|
||||
return true;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "surface-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
|
||||
private static void put(Map<BlockPos, BlockState> blocks,
|
||||
int x, int y, int z, BlockState state) {
|
||||
blocks.put(new BlockPos(x, y, z), state);
|
||||
}
|
||||
|
||||
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
|
||||
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorVegetationTest {
|
||||
@Test
|
||||
public void surfaceStructuresClearTreeColumnsAutomatically() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceStructuresPreserveVegetationUnlessConfigured() {
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allUndergroundGenerationStepsShareOneClassification() {
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.STRONGHOLDS));
|
||||
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundStructuresUseTheLowestTerrainColumn() {
|
||||
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||
int offset = NativeStructurePostProcessor.resolveBuriedOffset(
|
||||
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
|
||||
assertEquals(-5, offset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundStructuresFailWhenBurialWouldCrossWorldFloor() {
|
||||
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||
try {
|
||||
NativeStructurePostProcessor.resolveBuriedOffset(
|
||||
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100);
|
||||
} catch (IllegalStateException e) {
|
||||
assertTrue(e.getMessage().contains("world minimum"));
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Expected underground burial to fail below the world floor");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
String apiVersion = providers.gradleProperty('minecraftVersion').get()
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
compileOnly(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.paper.api)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(libs.sentry)
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
def pluginProperties = [
|
||||
name : rootProject.name,
|
||||
version : rootProject.version,
|
||||
apiVersion: apiVersion,
|
||||
main : mainClass,
|
||||
bootstrapper: bootstrapperClass,
|
||||
]
|
||||
inputs.properties(pluginProperties)
|
||||
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
||||
expand(pluginProperties)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('jar', Jar).configure {
|
||||
archiveBaseName.set('iris-bukkit-plugin')
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
||||
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisBootstrap implements PluginBootstrap {
|
||||
@Override
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
ProvisionResult provisioned = provision(context);
|
||||
Path datapackRoot = provisioned.datapackRoot();
|
||||
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context) {
|
||||
try {
|
||||
return DefaultPackBootstrapProvisioner.provision(
|
||||
context.getDataDirectory(),
|
||||
message -> context.getLogger().info(message)
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)")
|
||||
public class CommandDatapack implements DirectorExecutor {
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"})
|
||||
public void ingest(
|
||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
|
||||
boolean restart
|
||||
) {
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GRAY + "Starting datapack ingest...");
|
||||
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
|
||||
}
|
||||
|
||||
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"})
|
||||
public void list() {
|
||||
VolmitSender sender = sender();
|
||||
KList<String> configured = DatapackIngestService.collectConfiguredImports();
|
||||
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
|
||||
|
||||
sender.sendMessage(C.GREEN + "Configured datapack imports: " + C.WHITE + configured.size());
|
||||
for (String url : configured) {
|
||||
sender.sendMessage(C.GRAY + " - " + C.WHITE + url);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "Installed datapacks: " + C.WHITE + installed.size());
|
||||
for (DatapackIngestService.Entry entry : installed) {
|
||||
sender.sendMessage(C.GRAY + " - " + C.WHITE + entry.id + C.GRAY + " " + (entry.versionNumber == null ? "?" : entry.versionNumber));
|
||||
}
|
||||
|
||||
if (configured.isEmpty()) {
|
||||
sender.sendMessage(C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"})
|
||||
public void remove(
|
||||
@Param(description = "The datapack id (folder name) shown by /iris datapack list")
|
||||
String id
|
||||
) {
|
||||
DatapackIngestService.remove(sender(), id);
|
||||
}
|
||||
}
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.runtime.ChunkClearer;
|
||||
import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
|
||||
import art.arcane.iris.core.service.IrisEngineSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterContext;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.CountingDataInputStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAFile;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
|
||||
public class CommandDeveloper implements DirectorExecutor {
|
||||
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void EngineStatus() {
|
||||
Iris.service(IrisEngineSVC.class)
|
||||
.engineStatus(sender());
|
||||
}
|
||||
|
||||
@Director(description = "Send a test exception to sentry")
|
||||
public void Sentry() {
|
||||
Engine engine = engine();
|
||||
Exception testException = new Exception("This is a test");
|
||||
if (engine == null) {
|
||||
Iris.reportError(testException);
|
||||
return;
|
||||
}
|
||||
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
|
||||
Iris.reportError(testException);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", origin = DirectorOrigin.BOTH)
|
||||
public void genhash(
|
||||
@Param(description = "The world to hash", contextual = true)
|
||||
World world,
|
||||
@Param(description = "Radius in chunks around the center", defaultValue = "4")
|
||||
int radius,
|
||||
@Param(description = "Center chunk X", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(description = "Center chunk Z", defaultValue = "0")
|
||||
int centerZ) {
|
||||
if (world == null) {
|
||||
sender().sendMessage(C.RED + "World is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GREEN + "genhash started: " + ((radius * 2 + 1) * (radius * 2 + 1)) + " chunks...");
|
||||
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
|
||||
}
|
||||
|
||||
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
|
||||
long startMs = M.ms();
|
||||
int minY = world.getMinHeight();
|
||||
int maxY = world.getMaxHeight();
|
||||
long globalHash = 0L;
|
||||
long solidBlocks = 0L;
|
||||
Map<String, Long> histogram = new TreeMap<>();
|
||||
JsonObject chunkHashes = new JsonObject();
|
||||
|
||||
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
|
||||
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
|
||||
org.bukkit.ChunkSnapshot snapshot;
|
||||
try {
|
||||
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
|
||||
snapshot = loaded.getChunkSnapshot(false, false, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage(C.RED + "genhash failed at chunk " + rx + "," + rz + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
long chunkHash = 0L;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int worldX = (rx << 4) + x;
|
||||
int worldZ = (rz << 4) + z;
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
|
||||
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
|
||||
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
|
||||
^ ((long) worldZ * 0x165667B19E3779F9L);
|
||||
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
|
||||
chunkHash ^= blockHash;
|
||||
if (material != org.bukkit.Material.AIR
|
||||
&& material != org.bukkit.Material.CAVE_AIR
|
||||
&& material != org.bukkit.Material.VOID_AIR) {
|
||||
histogram.merge(material.name(), 1L, Long::sum);
|
||||
solidBlocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalHash ^= chunkHash;
|
||||
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
|
||||
}
|
||||
}
|
||||
|
||||
int side = radius * 2 + 1;
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("global", Long.toHexString(globalHash));
|
||||
result.addProperty("chunks", side * side);
|
||||
result.addProperty("solidBlocks", solidBlocks);
|
||||
result.addProperty("minY", minY);
|
||||
result.addProperty("maxY", maxY);
|
||||
JsonObject hist = new JsonObject();
|
||||
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
|
||||
hist.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
result.add("histogram", hist);
|
||||
result.add("chunkHashes", chunkHashes);
|
||||
|
||||
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
|
||||
try {
|
||||
Files.writeString(out.toPath(), result.toString());
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "genhash global=" + C.GOLD + Long.toHexString(globalHash)
|
||||
+ C.GREEN + " chunks=" + (side * side) + " solid=" + solidBlocks
|
||||
+ " in " + Form.duration((long) (M.ms() - startMs), 1));
|
||||
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
|
||||
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static long genHashMix(long z) {
|
||||
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
|
||||
return z ^ (z >>> 31);
|
||||
}
|
||||
|
||||
@Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", contextual = true)
|
||||
World world,
|
||||
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
|
||||
IrisDimension pack,
|
||||
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
|
||||
boolean confirm,
|
||||
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
|
||||
boolean freshDownload
|
||||
) {
|
||||
if (!confirm) {
|
||||
sender().sendMessage(new String[]{
|
||||
C.RED + "You should always make a backup before using this",
|
||||
C.YELLOW + "Issues caused by this can be, but are not limited to:",
|
||||
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
|
||||
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
|
||||
C.YELLOW + " - Structures not spawning again when regenerating",
|
||||
C.YELLOW + " - Caves not lining up",
|
||||
C.YELLOW + " - Terrain layers not lining up",
|
||||
C.RED + "Now that you are aware of the risks, and have made a back-up:",
|
||||
C.RED + "/iris developer update-world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
File folder = world.getWorldFolder();
|
||||
folder.mkdirs();
|
||||
|
||||
if (freshDownload) {
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
|
||||
}
|
||||
|
||||
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack, folder);
|
||||
}
|
||||
|
||||
@Director(description = "Test")
|
||||
public void mantle(
|
||||
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", defaultValue = "false")
|
||||
boolean plate,
|
||||
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", defaultValue = "21474836474")
|
||||
String name
|
||||
) throws Throwable {
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
sender().sendMessage(C.RED + "Target an Iris world before reading a mantle dump.");
|
||||
return;
|
||||
}
|
||||
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||
|
||||
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
|
||||
if (plate) {
|
||||
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
|
||||
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
Matter.read(section);
|
||||
}
|
||||
}
|
||||
if (!TectonicPlate.hasError()) {
|
||||
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test")
|
||||
public void packBenchmark(
|
||||
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Radius in regions", defaultValue = "2048")
|
||||
int radius,
|
||||
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
|
||||
boolean gui
|
||||
) {
|
||||
new IrisPackBenchmarking(dimension, radius, gui);
|
||||
}
|
||||
|
||||
@Director(description = "Upgrade to another Minecraft version")
|
||||
public void upgrade(
|
||||
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
|
||||
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
|
||||
ServerConfigurator.installDataPacks(version.get(), false);
|
||||
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
|
||||
}
|
||||
|
||||
@Director(description = "test")
|
||||
public void mca (
|
||||
@Param(description = "The world folder to scan for .mca region files") String world) {
|
||||
try {
|
||||
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||
for (File mca : McaFiles) {
|
||||
MCAFile MCARegion = MCAUtil.read(mca);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void deleteChunk(
|
||||
@Param(description = "Radius in chunks around your current chunk", defaultValue = "0")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "This is not an Iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(C.GREEN + "Delete started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ ". Clearing blocks to air.");
|
||||
|
||||
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
|
||||
}
|
||||
|
||||
@Director(description = "Test", aliases = {"ip"})
|
||||
public void network() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
|
||||
Iris.info("Display Name: %s", ni.getDisplayName());
|
||||
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
|
||||
for (InetAddress ia : Collections.list(inetAddresses)) {
|
||||
Iris.info("IP: %s", ia.getHostAddress());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Regen ---
|
||||
|
||||
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void regen(
|
||||
@Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world to use regen.");
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null. Generate nearby chunks first.");
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(C.GREEN + "Regen started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ ". Deleting and regenerating in place.");
|
||||
Iris.info("Regen run start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
|
||||
}
|
||||
|
||||
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH)
|
||||
public void goldenhash(
|
||||
@Param(description = "The world to scan", contextual = true)
|
||||
World world,
|
||||
@Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8")
|
||||
int radius,
|
||||
@Param(name = "center-x", description = "Center chunk X", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(name = "center-z", description = "Center chunk Z", defaultValue = "0")
|
||||
int centerZ,
|
||||
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true")
|
||||
boolean resetMantle,
|
||||
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8")
|
||||
int threads,
|
||||
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false")
|
||||
boolean deep
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "Target must be an Iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ " in buffers (world untouched).");
|
||||
Iris.info("goldenhash start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Desktop.isDesktopSupported()) {
|
||||
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
|
||||
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (biome == null || biome.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(biome.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
|
||||
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (region == null || region.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(region.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
|
||||
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dimension == null || dimension.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(dimension.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for")
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for")
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a point of interest.")
|
||||
public void poi(
|
||||
@Param(description = "The type of PoI to look for.")
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
|
||||
String structure
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
if (commandSender == null) {
|
||||
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
String structureKey = structure == null ? "" : structure.trim();
|
||||
Structure nativeStructure = resolveNativeStructure(structureKey);
|
||||
if (nativeStructure != null) {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||
e, structureKey, false);
|
||||
if (!decision.generate()
|
||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
structureKey, decision.status()));
|
||||
return;
|
||||
}
|
||||
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeStructure == null) {
|
||||
commandSender.sendMessage(C.RED + "Unknown structure: " + structureKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
|
||||
return;
|
||||
}
|
||||
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structureKey + "...");
|
||||
J.s(() -> {
|
||||
try {
|
||||
if (!StructureReachability.isReachable(e, structureKey)) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + structureKey + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
StructureSearchResult result = targetWorld.locateNearestStructure(
|
||||
origin, nativeStructure, 100, false);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structureKey + " found within range of you.");
|
||||
return;
|
||||
}
|
||||
prepareStructureTeleport(
|
||||
target, targetWorld, commandSender, structureKey, result.getLocation(), false);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate structure '" + structureKey + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Structure resolveNativeStructure(String structureKey) {
|
||||
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
|
||||
for (Structure candidate : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(candidate);
|
||||
if (key != null && key.toString().equalsIgnoreCase(structureKey)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
|
||||
return;
|
||||
}
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
int blockX = origin.getBlockX();
|
||||
int blockZ = origin.getBlockZ();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
|
||||
return;
|
||||
}
|
||||
if (!result.found()) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
targetWorld, result.originX(), result.baseY(), result.originZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find an object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
Player studioPlayer = player();
|
||||
if (studioPlayer != null) {
|
||||
try {
|
||||
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
|
||||
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Iris.reportError(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.hasObjectPlacement(object)) {
|
||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
|
||||
}
|
||||
|
||||
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
try {
|
||||
int y = useLocatedY
|
||||
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
|
||||
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
J.runEntity(target, () -> {
|
||||
BukkitPlatform.teleportAsync(target, destination);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ "
|
||||
+ at.getBlockX() + ", " + y + ", " + at.getBlockZ());
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
|
||||
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||
}
|
||||
}
|
||||
+293
-191
@@ -16,66 +16,86 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.nms.INMS;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisDimension;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.decree.specialhandlers.NullablePlayerHandler;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.misc.ServerProperties;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import lombok.SneakyThrows;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static com.volmit.iris.core.service.EditSVC.deletingWorld;
|
||||
import static com.volmit.iris.util.misc.ServerProperties.BUKKIT_YML;
|
||||
import static art.arcane.iris.core.service.EditSVC.deletingWorld;
|
||||
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||
import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
@Decree(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
|
||||
public class CommandIris implements DecreeExecutor {
|
||||
private CommandUpdater updater;
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
private CommandStudio studio;
|
||||
private CommandPregen pregen;
|
||||
private CommandSettings settings;
|
||||
private CommandObject object;
|
||||
private CommandJigsaw jigsaw;
|
||||
private CommandStructure structure;
|
||||
private CommandWhat what;
|
||||
private CommandEdit edit;
|
||||
private CommandFind find;
|
||||
private CommandDeveloper developer;
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
private CommandDatapack datapack;
|
||||
public static boolean worldCreation = false;
|
||||
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
|
||||
String WorldEngine;
|
||||
String worldNameToCheck = "YourWorldName";
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Decree(description = "Create a new world", aliases = {"+", "c"})
|
||||
@Director(description = "Create a new world", aliases = {"c"})
|
||||
public void create(
|
||||
@Param(aliases = "world-name", description = "The name of the world to create")
|
||||
String name,
|
||||
@Param(aliases = "dimension", description = "The dimension type to create the world with", defaultValue = "default")
|
||||
IrisDimension type,
|
||||
@Param(
|
||||
aliases = {"dimension", "pack"},
|
||||
description = "The dimension/pack to create the world with",
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type,
|
||||
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
||||
long seed,
|
||||
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
||||
@@ -93,15 +113,34 @@ public class CommandIris implements DecreeExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new File(Bukkit.getWorldContainer(), name).exists()) {
|
||||
if (IrisWorldStorage.dimensionRoot(name).exists()) {
|
||||
sender().sendMessage(C.RED + "That folder already exists!");
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedType = type.equalsIgnoreCase("default")
|
||||
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||
: type;
|
||||
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
|
||||
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
|
||||
sender().sendMessage(C.YELLOW + "Or download manually: /iris download " + resolvedType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
if (stageFoliaWorldCreation(name, dimension, seed, main)) {
|
||||
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
worldCreation = true;
|
||||
IrisToolbelt.createWorld()
|
||||
.dimension(type.getLoadKey())
|
||||
.dimension(resolvedType)
|
||||
.name(name)
|
||||
.seed(seed)
|
||||
.sender(sender())
|
||||
@@ -115,34 +154,127 @@ public class CommandIris implements DecreeExecutor {
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
|
||||
Iris.error("Exception raised during world creation: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
|
||||
worldCreation = false;
|
||||
return;
|
||||
}
|
||||
|
||||
worldCreation = false;
|
||||
sender().sendMessage(C.GREEN + "Successfully created your world!");
|
||||
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void updateMainWorld(String newName) {
|
||||
File worlds = Bukkit.getWorldContainer();
|
||||
var data = ServerProperties.DATA;
|
||||
try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.load(in);
|
||||
}
|
||||
for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) {
|
||||
IO.copyDirectory(new File(worlds, ServerProperties.LEVEL_NAME + "/" + sub).toPath(), new File(worlds, newName + "/" + sub).toPath());
|
||||
}
|
||||
private boolean updateMainWorld(String newName) {
|
||||
try {
|
||||
File oldLevelRoot = IrisWorldStorage.levelRoot();
|
||||
File worldContainer = oldLevelRoot.getParentFile();
|
||||
Properties data = ServerProperties.DATA;
|
||||
try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.load(in);
|
||||
}
|
||||
|
||||
data.setProperty("level-name", newName);
|
||||
try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.store(out, null);
|
||||
File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromLegacyName(newName));
|
||||
if (!sourceDimensionRoot.isDirectory()) {
|
||||
throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath());
|
||||
}
|
||||
|
||||
File newLevelRoot = new File(worldContainer, newName);
|
||||
if (!newLevelRoot.exists() && !newLevelRoot.mkdirs()) {
|
||||
throw new IllegalStateException("Could not create target level folder: " + newLevelRoot.getAbsolutePath());
|
||||
}
|
||||
|
||||
for (String sub : List.of("data", "datapacks", "players")) {
|
||||
File source = new File(oldLevelRoot, sub);
|
||||
if (!source.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IO.copyDirectory(source.toPath(), new File(newLevelRoot, sub).toPath());
|
||||
}
|
||||
|
||||
File targetDimensionRoot = IrisWorldStorage.dimensionRoot(newLevelRoot, NamespacedKey.minecraft("overworld"));
|
||||
IO.copyDirectory(sourceDimensionRoot.toPath(), targetDimensionRoot.toPath());
|
||||
|
||||
World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(newName)).orElse(null);
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName);
|
||||
if (sourceWorld == null && stagedSeed == null) {
|
||||
throw new IllegalStateException("Cannot determine the promoted world's seed.");
|
||||
}
|
||||
long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed();
|
||||
data.setProperty("level-name", newName);
|
||||
data.setProperty("level-seed", Long.toString(promotedSeed));
|
||||
try (FileOutputStream out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.store(out, null);
|
||||
}
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
|
||||
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
|
||||
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
||||
|
||||
File worldFolder = IrisWorldStorage.dimensionRoot(name);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
|
||||
if (installed == null) {
|
||||
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (main) {
|
||||
if (updateMainWorld(name)) {
|
||||
sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\".");
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
|
||||
if (main) {
|
||||
sender().sendMessage(C.GREEN + "This world is now configured as main for next restart.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
|
||||
if (worlds == null) {
|
||||
worlds = yml.createSection("worlds");
|
||||
}
|
||||
ConfigurationSection worldSection = worlds.getConfigurationSection(worldName);
|
||||
if (worldSection == null) {
|
||||
worldSection = worlds.createSection(worldName);
|
||||
}
|
||||
|
||||
String generator = "Iris:" + dimension;
|
||||
worldSection.set("generator", generator);
|
||||
if (seed != null) {
|
||||
worldSection.set("seed", seed);
|
||||
}
|
||||
|
||||
try {
|
||||
yml.save(BUKKIT_YML);
|
||||
Iris.info("Registered \"" + worldName + "\" in bukkit.yml");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage());
|
||||
Iris.error("Failed to update bukkit.yml!");
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||
public void teleport(
|
||||
@Param(description = "World to teleport to")
|
||||
World world,
|
||||
@@ -158,23 +290,24 @@ public class CommandIris implements DecreeExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
target.teleport(world.getSpawnLocation());
|
||||
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
|
||||
}
|
||||
}.runTask(Iris.instance);
|
||||
final Location spawn = world.getSpawnLocation();
|
||||
final Runnable teleportTask = () -> {
|
||||
BukkitPlatform.teleportAsync(target, spawn);
|
||||
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
|
||||
};
|
||||
if (!J.runEntity(target, teleportTask)) {
|
||||
teleportTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Print version information")
|
||||
@Director(description = "Print version information")
|
||||
public void version() {
|
||||
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
|
||||
}
|
||||
|
||||
/*
|
||||
/todo
|
||||
@Decree(description = "Benchmark a pack", origin = DecreeOrigin.CONSOLE)
|
||||
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
|
||||
public void packbenchmark(
|
||||
@Param(description = "Dimension to benchmark")
|
||||
IrisDimension type
|
||||
@@ -185,7 +318,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
IrisPackBenchmarking.runBenchmark();
|
||||
} */
|
||||
|
||||
@Decree(description = "Print world height information", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "Print world height information", origin = DirectorOrigin.PLAYER)
|
||||
public void height() {
|
||||
if (sender().isPlayer()) {
|
||||
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
|
||||
@@ -197,13 +330,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "QOL command to open a overworld studio world.", sync = true)
|
||||
public void so() {
|
||||
sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)");
|
||||
Iris.service(StudioSVC.class).open(sender(), 1337, "overworld");
|
||||
}
|
||||
|
||||
@Decree(description = "Check access of all worlds.", aliases = {"accesslist"})
|
||||
@Director(description = "Check access of all worlds.", aliases = {"accesslist"})
|
||||
public void worlds() {
|
||||
KList<World> IrisWorlds = new KList<>();
|
||||
KList<World> BukkitWorlds = new KList<>();
|
||||
@@ -241,7 +368,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true)
|
||||
@Director(description = "Remove an Iris world", aliases = {"rm"}, sync = true)
|
||||
public void remove(
|
||||
@Param(description = "The world to remove")
|
||||
World world,
|
||||
@@ -259,7 +386,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Bukkit.unloadWorld(world, false)) {
|
||||
if (!WorldLifecycleService.get().unload(world, false)) {
|
||||
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
|
||||
return;
|
||||
}
|
||||
@@ -272,7 +399,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
|
||||
}
|
||||
IrisToolbelt.evacuate(world, "Deleting world");
|
||||
deletingWorld = true;
|
||||
@@ -317,81 +444,34 @@ public class CommandIris implements DecreeExecutor {
|
||||
return dir.delete();
|
||||
}
|
||||
|
||||
@Decree(description = "Set aura spins")
|
||||
public void aura(
|
||||
@Param(description = "The h color value", defaultValue = "-20")
|
||||
int h,
|
||||
@Param(description = "The s color value", defaultValue = "7")
|
||||
int s,
|
||||
@Param(description = "The b color value", defaultValue = "8")
|
||||
int b
|
||||
) {
|
||||
IrisSettings.get().getGeneral().setSpinh(h);
|
||||
IrisSettings.get().getGeneral().setSpins(s);
|
||||
IrisSettings.get().getGeneral().setSpinb(b);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage("<rainbow>Aura Spins updated to " + h + " " + s + " " + b);
|
||||
}
|
||||
|
||||
@Decree(description = "Bitwise calculations")
|
||||
public void bitwise(
|
||||
@Param(description = "The first value to run calculations on")
|
||||
int value1,
|
||||
@Param(description = "The operator: | & ^ ≺≺ ≻≻ %")
|
||||
String operator,
|
||||
@Param(description = "The second value to run calculations on")
|
||||
int value2
|
||||
) {
|
||||
Integer v = null;
|
||||
switch (operator) {
|
||||
case "|" -> v = value1 | value2;
|
||||
case "&" -> v = value1 & value2;
|
||||
case "^" -> v = value1 ^ value2;
|
||||
case "%" -> v = value1 % value2;
|
||||
case ">>" -> v = value1 >> value2;
|
||||
case "<<" -> v = value1 << value2;
|
||||
}
|
||||
if (v == null) {
|
||||
sender().sendMessage(C.RED + "The operator you entered: (" + operator + ") is invalid!");
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v);
|
||||
}
|
||||
|
||||
@Decree(description = "Toggle debug")
|
||||
public void debug(
|
||||
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
|
||||
Boolean on
|
||||
) {
|
||||
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
|
||||
@Director(description = "Toggle debug")
|
||||
public void debug() {
|
||||
boolean to = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(to);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(C.GREEN + "Set debug to: " + to);
|
||||
}
|
||||
|
||||
//TODO fix pack trimming
|
||||
@Decree(description = "Download a project.", aliases = "dl")
|
||||
@Director(description = "Download a project.", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||
@Param(name = "pack", description = "The pack to download", aliases = "project")
|
||||
String pack,
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "main")
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
||||
String branch,
|
||||
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||
//boolean trim,
|
||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||
boolean overwrite
|
||||
) {
|
||||
boolean trim = false;
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : ""));
|
||||
if (pack.equals("overworld")) {
|
||||
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
|
||||
Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite);
|
||||
if (PackDownloader.isDefaultOverworld(pack)) {
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + " (beta release)" + (overwrite ? " overwriting" : ""));
|
||||
Iris.service(StudioSVC.class).downloadDefaultOverworld(sender(), overwrite);
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite);
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
|
||||
}
|
||||
ServerConfigurator.installDataPacksIfChanged(true);
|
||||
}
|
||||
|
||||
@Decree(description = "Get metrics for your world", aliases = "measure", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER)
|
||||
public void metrics() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
@@ -401,50 +481,15 @@ public class CommandIris implements DecreeExecutor {
|
||||
engine().printMetrics(sender());
|
||||
}
|
||||
|
||||
@Decree(description = "Reload configuration file (this is also done automatically)")
|
||||
@Director(description = "Reload configuration file (this is also done automatically)")
|
||||
public void reload() {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
sender().sendMessage(C.GREEN + "Hotloaded settings");
|
||||
}
|
||||
|
||||
@Decree(description = "Update the pack of a world (UNSAFE!)", name = "^world", aliases = "update-world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", contextual = true)
|
||||
World world,
|
||||
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
|
||||
IrisDimension pack,
|
||||
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
|
||||
boolean confirm,
|
||||
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
|
||||
boolean freshDownload
|
||||
) {
|
||||
if (!confirm) {
|
||||
sender().sendMessage(new String[]{
|
||||
C.RED + "You should always make a backup before using this",
|
||||
C.YELLOW + "Issues caused by this can be, but are not limited to:",
|
||||
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
|
||||
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
|
||||
C.YELLOW + " - Structures not spawning again when regenerating",
|
||||
C.YELLOW + " - Caves not lining up",
|
||||
C.YELLOW + " - Terrain layers not lining up",
|
||||
C.RED + "Now that you are aware of the risks, and have made a back-up:",
|
||||
C.RED + "/iris ^world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
File folder = world.getWorldFolder();
|
||||
folder.mkdirs();
|
||||
|
||||
if (freshDownload) {
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true);
|
||||
}
|
||||
|
||||
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder);
|
||||
}
|
||||
|
||||
@Decree(description = "Unload an Iris World", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
@Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void unloadWorld(
|
||||
@Param(description = "The world to unload")
|
||||
World world
|
||||
@@ -456,20 +501,23 @@ public class CommandIris implements DecreeExecutor {
|
||||
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
|
||||
try {
|
||||
IrisToolbelt.evacuate(world);
|
||||
Bukkit.unloadWorld(world, false);
|
||||
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
||||
boolean unloaded = WorldLifecycleService.get().unload(world, false);
|
||||
if (unloaded) {
|
||||
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "Failed to unload the world.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Load an Iris World", origin = DecreeOrigin.PLAYER, sync = true, aliases = {"import"})
|
||||
@Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
|
||||
public void loadWorld(
|
||||
@Param(description = "The name of the world to load")
|
||||
String world
|
||||
) {
|
||||
World worldloaded = Bukkit.getWorld(world);
|
||||
worldNameToCheck = world;
|
||||
boolean worldExists = doesWorldExist(worldNameToCheck);
|
||||
WorldEngine = world;
|
||||
@@ -479,8 +527,7 @@ public class CommandIris implements DecreeExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
String pathtodim = world + File.separator +"iris"+File.separator +"pack"+File.separator +"dimensions"+File.separator;
|
||||
File directory = new File(Bukkit.getWorldContainer(), pathtodim);
|
||||
File directory = new File(IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(world)), "dimensions");
|
||||
|
||||
String dimension = null;
|
||||
if (directory.exists() && directory.isDirectory()) {
|
||||
@@ -500,26 +547,27 @@ public class CommandIris implements DecreeExecutor {
|
||||
sender().sendMessage(C.GOLD + world + " is not an iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Loading world: " + world);
|
||||
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||
String gen = "Iris:" + dimension;
|
||||
ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds");
|
||||
if (!section.contains(world)) {
|
||||
section.createSection(world).set("generator", gen);
|
||||
try {
|
||||
yml.save(BUKKIT_YML);
|
||||
Iris.info("Registered \"" + world + "\" in bukkit.yml");
|
||||
} catch (IOException e) {
|
||||
Iris.error("Failed to update bukkit.yml!");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (!registerWorldInBukkitYml(world, dimension, null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\".");
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.instance.checkForBukkitWorlds(world::equals);
|
||||
sender().sendMessage(C.GREEN + world + " loaded successfully.");
|
||||
}
|
||||
@Decree(description = "Evacuate an iris world", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
@Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void evacuate(
|
||||
@Param(description = "Evacuate the world")
|
||||
World world
|
||||
@@ -533,8 +581,62 @@ public class CommandIris implements DecreeExecutor {
|
||||
}
|
||||
|
||||
boolean doesWorldExist(String worldName) {
|
||||
File worldContainer = Bukkit.getWorldContainer();
|
||||
File worldDirectory = new File(worldContainer, worldName);
|
||||
File worldDirectory = IrisWorldStorage.dimensionRoot(worldName);
|
||||
return worldDirectory.exists() && worldDirectory.isDirectory();
|
||||
}
|
||||
|
||||
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
Set<String> options = new LinkedHashSet<>();
|
||||
options.add("default");
|
||||
|
||||
File packsFolder = Iris.instance.getDataFolder("packs");
|
||||
File[] packs = packsFolder.listFiles();
|
||||
if (packs != null) {
|
||||
for (File pack : packs) {
|
||||
if (pack == null || !pack.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.add(pack.getName());
|
||||
|
||||
try {
|
||||
IrisData data = IrisData.get(pack);
|
||||
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
||||
options.add(key);
|
||||
options.add(pack.getName() + ":" + key);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
|
||||
pack.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new KList<>(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String in, boolean force) throws DirectorParsingException {
|
||||
if (in == null || in.trim().isEmpty()) {
|
||||
throw new DirectorParsingException("World type cannot be empty");
|
||||
}
|
||||
|
||||
return in.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
+236
-49
@@ -16,41 +16,169 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.link.WorldEditLink;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.service.ObjectSVC;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.core.service.WandSVC;
|
||||
import com.volmit.iris.core.tools.IrisConverter;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.*;
|
||||
import com.volmit.iris.util.data.Cuboid;
|
||||
import com.volmit.iris.util.data.IrisCustomData;
|
||||
import com.volmit.iris.util.data.registry.Materials;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.Direction;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import org.bukkit.*;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.link.WorldEditLink;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.service.ObjectSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.service.WandSVC;
|
||||
import art.arcane.iris.core.tools.IrisConverter;
|
||||
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.StudioMode;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.data.registry.Materials;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.math.Direction;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
||||
public class CommandObject implements DecreeExecutor {
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", sync = true)
|
||||
public void studio(
|
||||
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Map<String, IrisData> sources = new LinkedHashMap<>();
|
||||
IrisDimension hostDimension = dimension;
|
||||
|
||||
if (dimension != null) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
|
||||
}
|
||||
sources.put(data.getDataFolder().getName(), data);
|
||||
} else {
|
||||
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||
File[] packs = workspace == null ? null : workspace.listFiles();
|
||||
if (packs != null) {
|
||||
Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
|
||||
for (File pack : packs) {
|
||||
if (!pack.isDirectory()) continue;
|
||||
File dimensionsDir = new File(pack, "dimensions");
|
||||
if (!dimensionsDir.isDirectory()) continue;
|
||||
IrisData data = IrisData.get(pack);
|
||||
String[] keys = data.getObjectLoader().getPossibleKeys();
|
||||
if (keys == null || keys.length == 0) continue;
|
||||
sources.put(pack.getName(), data);
|
||||
if (hostDimension == null) {
|
||||
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
|
||||
if (dimFiles != null && dimFiles.length > 0) {
|
||||
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
|
||||
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
|
||||
if (loaded != null) {
|
||||
hostDimension = loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hostDimension == null || sources.isEmpty()) {
|
||||
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalObjects = 0;
|
||||
for (IrisData d : sources.values()) {
|
||||
String[] k = d.getObjectLoader().getPossibleKeys();
|
||||
if (k != null) totalObjects += k.length;
|
||||
}
|
||||
if (totalObjects == 0) {
|
||||
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
|
||||
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
||||
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
||||
|
||||
String scope = dimension == null
|
||||
? ("all packs [" + sources.size() + "]")
|
||||
: ("\"" + hostDimension.getName() + "\"");
|
||||
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
|
||||
+ totalObjects + " objects)");
|
||||
|
||||
IrisDimension finalHost = hostDimension;
|
||||
try {
|
||||
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
|
||||
if (world == null) return;
|
||||
try {
|
||||
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
|
||||
}
|
||||
|
||||
if (commandSender.isPlayer()) {
|
||||
Player p = commandSender.player();
|
||||
if (p != null) {
|
||||
Location target = new Location(world, 0.5D, 66D, 0.5D);
|
||||
J.runEntity(p, () -> {
|
||||
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
|
||||
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
|
||||
Material.POPPY, Material.DANDELION);
|
||||
@@ -69,7 +197,8 @@ public class CommandObject implements DecreeExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
public void set(int x, int y, int z, PlatformBlockState s) {
|
||||
BlockData d = (BlockData) s.nativeHandle();
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
|
||||
//Prevent blocks being set in or bellow bedrock
|
||||
@@ -84,8 +213,8 @@ public class CommandObject implements DecreeExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getBlockData();
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,7 +269,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
@Decree(description = "Check the composition of an object")
|
||||
@Director(description = "Check the composition of an object")
|
||||
public void analyze(
|
||||
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
|
||||
String object
|
||||
@@ -154,7 +283,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
Map<BlockData, Integer> amounts = new HashMap<>();
|
||||
Map<Material, Integer> materials = new HashMap<>();
|
||||
while (queue.hasNext()) {
|
||||
BlockData block = queue.next();
|
||||
BlockData block = (BlockData) queue.next().nativeHandle();
|
||||
|
||||
//unsorted.put(block.getMaterial(), block);
|
||||
|
||||
@@ -208,7 +337,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Shrink an object to its minimum size")
|
||||
@Director(description = "Shrink an object to its minimum size")
|
||||
public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
|
||||
@@ -222,7 +351,64 @@ public class CommandObject implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Convert .schem files in the 'convert' folder to .iob files.")
|
||||
@Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay",
|
||||
origin = DirectorOrigin.BOTH)
|
||||
public void plausibilize(
|
||||
@Param(description = "Object key, prefix (trees/), or filesystem path",
|
||||
customHandler = ObjectTargetHandler.class)
|
||||
String target,
|
||||
@Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", defaultValue = "false")
|
||||
boolean dryRun,
|
||||
@Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", defaultValue = "12")
|
||||
int reach
|
||||
) {
|
||||
IrisData nearest = data();
|
||||
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, nearest);
|
||||
if (targets.isEmpty() && nearest == null) {
|
||||
targets = resolveFromPacks(target);
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "No objects matched: " + target);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.IRIS + "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
|
||||
+ "] queued " + targets.size() + " object(s)");
|
||||
|
||||
org.bukkit.command.CommandSender s = sender();
|
||||
List<TreePlausibilizeBatch.Target> queued = targets;
|
||||
J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (String line) ->
|
||||
s.sendMessage(line.startsWith("Done:") || line.startsWith("Totals:") || line.startsWith("[")
|
||||
? C.IRIS + line
|
||||
: C.GRAY + " " + line)));
|
||||
}
|
||||
|
||||
private static List<TreePlausibilizeBatch.Target> resolveFromPacks(String target) {
|
||||
List<TreePlausibilizeBatch.Target> out = new ArrayList<>();
|
||||
File packsFolder = Iris.instance.getDataFolder("packs");
|
||||
File[] packs = packsFolder.listFiles(File::isDirectory);
|
||||
if (packs == null) {
|
||||
return out;
|
||||
}
|
||||
for (File pack : packs) {
|
||||
File objectsRoot = new File(pack, "objects");
|
||||
if (!objectsRoot.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
File candidate = new File(objectsRoot, target + ".iob");
|
||||
if (candidate.isFile()) {
|
||||
out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate));
|
||||
continue;
|
||||
}
|
||||
File candidateDir = new File(objectsRoot, target);
|
||||
if (candidateDir.isDirectory()) {
|
||||
TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
|
||||
public void convert () {
|
||||
try {
|
||||
IrisConverter.convertSchematics(sender());
|
||||
@@ -232,13 +418,13 @@ public class CommandObject implements DecreeExecutor {
|
||||
|
||||
}
|
||||
|
||||
@Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d")
|
||||
@Director(description = "Get a powder that reveals objects", aliases = "d")
|
||||
public void dust() {
|
||||
player().getInventory().addItem(WandSVC.createDust());
|
||||
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
}
|
||||
|
||||
@Decree(description = "Contract a selection based on your looking direction", aliases = "-")
|
||||
@Director(description = "Contract a selection based on your looking direction", aliases = "-")
|
||||
public void contract(
|
||||
@Param(description = "The amount to inset by", defaultValue = "1")
|
||||
int amount
|
||||
@@ -259,7 +445,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d, -amount);
|
||||
cursor = cursor.expand(d.f(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
@@ -267,7 +453,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Decree(description = "Set point 1 to look", aliases = "p1")
|
||||
@Director(description = "Set point 1 to look", aliases = "p1")
|
||||
public void position1(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
@@ -289,11 +475,11 @@ public class CommandObject implements DecreeExecutor {
|
||||
} else {
|
||||
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Set point 2 to look", aliases = "p2")
|
||||
@Director(description = "Set point 2 to look", aliases = "p2")
|
||||
public void position2(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
@@ -316,11 +502,11 @@ public class CommandObject implements DecreeExecutor {
|
||||
} else {
|
||||
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Paste an object", sync = true)
|
||||
@Director(description = "Paste an object", sync = true)
|
||||
public void paste(
|
||||
@Param(description = "The object to paste", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@@ -360,8 +546,9 @@ public class CommandObject implements DecreeExecutor {
|
||||
Iris.service(ObjectSVC.class).addChanges(futureChanges);
|
||||
|
||||
if (edit) {
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(o.getCenter()).add(o.getW() - 1,
|
||||
o.getH() + o.getCenter().clone().getY() - 1, o.getD() - 1), block.clone().subtract(o.getCenter().clone().setY(0)));
|
||||
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
|
||||
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
|
||||
if (WandSVC.isWand(wand)) {
|
||||
wand = newWand;
|
||||
player().getInventory().setItemInMainHand(wand);
|
||||
@@ -381,7 +568,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Save an object")
|
||||
@Director(description = "Save an object")
|
||||
public void save(
|
||||
@Param(description = "The dimension to store the object in", contextual = true)
|
||||
IrisDimension dimension,
|
||||
@@ -416,7 +603,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name);
|
||||
}
|
||||
|
||||
@Decree(description = "Shift a selection in your looking direction", aliases = "-")
|
||||
@Director(description = "Shift a selection in your looking direction")
|
||||
public void shift(
|
||||
@Param(description = "The amount to shift by", defaultValue = "1")
|
||||
int amount
|
||||
@@ -447,7 +634,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Decree(description = "Undo a number of pastes", aliases = "-")
|
||||
@Director(description = "Undo a number of pastes", aliases = "u")
|
||||
public void undo(
|
||||
@Param(description = "The amount of pastes to undo", defaultValue = "1")
|
||||
int amount
|
||||
@@ -458,7 +645,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
|
||||
}
|
||||
|
||||
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER)
|
||||
public void we() {
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know.");
|
||||
@@ -476,14 +663,14 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!");
|
||||
}
|
||||
|
||||
@Decree(description = "Get an object wand", sync = true)
|
||||
@Director(description = "Get an object wand", sync = true)
|
||||
public void wand() {
|
||||
player().getInventory().addItem(WandSVC.createWand());
|
||||
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
|
||||
sender().sendMessage(C.GREEN + "Poof! Good luck building!");
|
||||
}
|
||||
|
||||
@Decree(name = "x&y", description = "Autoselect up, down & out", sync = true)
|
||||
@Director(name = "x&y", description = "Autoselect up, down & out", sync = true)
|
||||
public void xay() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
@@ -534,7 +721,7 @@ public class CommandObject implements DecreeExecutor {
|
||||
sender().sendMessage(C.GREEN + "Auto-select complete!");
|
||||
}
|
||||
|
||||
@Decree(name = "x+y", description = "Autoselect up & out", sync = true)
|
||||
@Director(name = "x+y", description = "Autoselect up & out", sync = true)
|
||||
public void xpy() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"})
|
||||
public void validate(
|
||||
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packsRoot = Iris.instance.getDataFolder("packs");
|
||||
if (!packsRoot.isDirectory()) {
|
||||
s.sendMessage(C.RED + "packs/ folder not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pack == null || pack.isBlank()) {
|
||||
File[] dirs = packsRoot.listFiles(File::isDirectory);
|
||||
if (dirs == null || dirs.length == 0) {
|
||||
s.sendMessage(C.YELLOW + "No packs to validate.");
|
||||
return;
|
||||
}
|
||||
int broken = 0;
|
||||
for (File dir : dirs) {
|
||||
PackValidationResult result = runValidate(s, dir);
|
||||
if (result != null && !result.isLoadable()) {
|
||||
broken++;
|
||||
}
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
|
||||
return;
|
||||
}
|
||||
|
||||
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||
if (target == null) {
|
||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return;
|
||||
}
|
||||
runValidate(s, target);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||
if (!result.success()) {
|
||||
s.sendMessage(C.RED + result.error());
|
||||
reportPaths(s, result.quarantinedPaths(), "still quarantined");
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Quarantined " + result.quarantinedPaths().size()
|
||||
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
|
||||
reportPaths(s, result.quarantinedPaths(), "quarantined");
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(C.RED + "Cleanup mode must be preview or apply.");
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(C.RED + preview.error());
|
||||
return;
|
||||
}
|
||||
if (!preview.hasCandidates()) {
|
||||
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.YELLOW + "Cleanup preview for pack '" + pack + "': "
|
||||
+ preview.candidatePaths().size() + " candidate(s). No files were changed.");
|
||||
reportPaths(s, preview.candidatePaths(), "candidate");
|
||||
s.sendMessage(C.GRAY + "Run /iris pack cleanup " + pack + " mode=apply to quarantine these candidates after a fresh scan.");
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply restoration of the latest quarantine", aliases = {"r"})
|
||||
public void restore(
|
||||
@Param(description = "The pack folder name to restore")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
s.sendMessage(C.RED + "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
|
||||
reportPaths(s, result.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
if (!result.success()) {
|
||||
s.sendMessage(C.RED + result.error());
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Restored " + result.restoredPaths().size()
|
||||
+ " file(s) from " + result.dumpPath() + ".");
|
||||
reportPaths(s, result.restoredPaths(), "restored");
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(C.RED + "Restore mode must be preview or apply.");
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(C.RED + preview.error());
|
||||
return;
|
||||
}
|
||||
if (!preview.hasFiles()) {
|
||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.YELLOW + "Restore preview for " + preview.dumpPath() + ": "
|
||||
+ preview.filePaths().size() + " file(s). No files were changed.");
|
||||
reportPaths(s, preview.filePaths(), "file");
|
||||
if (!preview.conflicts().isEmpty()) {
|
||||
s.sendMessage(C.RED + "Restore is blocked by " + preview.conflicts().size() + " existing destination(s).");
|
||||
reportPaths(s, preview.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GRAY + "Run /iris pack restore " + pack + " mode=apply to restore after a fresh conflict check.");
|
||||
}
|
||||
|
||||
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
|
||||
public void status(
|
||||
@Param(description = "The pack folder name", defaultValue = "")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (pack == null || pack.isBlank()) {
|
||||
if (PackValidationRegistry.snapshot().isEmpty()) {
|
||||
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.snapshot().forEach((name, result) -> {
|
||||
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
|
||||
s.sendMessage(tag + C.RESET + " " + name
|
||||
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
|
||||
+ ", warnings=" + result.getWarnings().size() + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
PackValidationResult result = PackValidationRegistry.get(pack);
|
||||
if (result == null) {
|
||||
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
|
||||
return;
|
||||
}
|
||||
reportResult(s, result);
|
||||
}
|
||||
|
||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packFolder);
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void reportResult(VolmitSender s, PackValidationResult result) {
|
||||
if (result.isLoadable()) {
|
||||
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
|
||||
+ C.GRAY + " (warnings=" + result.getWarnings().size() + ")");
|
||||
} else {
|
||||
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
s.sendMessage(C.RED + " - " + reason);
|
||||
}
|
||||
}
|
||||
int wMax = Math.min(10, result.getWarnings().size());
|
||||
for (int i = 0; i < wMax; i++) {
|
||||
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
|
||||
}
|
||||
if (result.getWarnings().size() > wMax) {
|
||||
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
|
||||
}
|
||||
}
|
||||
|
||||
private File findPack(VolmitSender sender, String pack) {
|
||||
if (pack == null || pack.isBlank()) {
|
||||
sender.sendMessage(C.RED + "You must specify a pack name.");
|
||||
return null;
|
||||
}
|
||||
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
|
||||
if (packFolder == null) {
|
||||
sender.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return null;
|
||||
}
|
||||
return packFolder;
|
||||
}
|
||||
|
||||
private void reportPaths(VolmitSender sender, List<String> paths, String label) {
|
||||
int max = Math.min(10, paths.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
sender.sendMessage(C.GRAY + " - " + label + ": " + paths.get(i));
|
||||
}
|
||||
if (paths.size() > max) {
|
||||
sender.sendMessage(C.GRAY + " ... and " + (paths.size() - max) + " more.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", contextual = true)
|
||||
World world,
|
||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||
Vector center,
|
||||
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
|
||||
boolean gui,
|
||||
@Param(name = "serial", description = "Generate only one chunk at a time", defaultValue = "false")
|
||||
boolean serial
|
||||
) {
|
||||
if (radius <= 0) {
|
||||
sender().sendMessage(C.RED + "Pregen radius must be greater than zero blocks.");
|
||||
return;
|
||||
}
|
||||
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
|
||||
sender().sendMessage(C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (sender().isPlayer() && access() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||
}
|
||||
PregenTask task = PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build();
|
||||
if (serial) {
|
||||
IrisToolbelt.pregenerateSerial(task, world);
|
||||
} else {
|
||||
IrisToolbelt.pregenerate(task, world);
|
||||
}
|
||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||
sender().sendMessage(msg);
|
||||
Iris.info(msg);
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Failed to start pregeneration. See console for details.");
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
||||
public void stop() {
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
|
||||
sender().sendMessage(message);
|
||||
Iris.info(message);
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Show the active pregeneration status")
|
||||
public void status() {
|
||||
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
|
||||
if (progress == null) {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration task.");
|
||||
return;
|
||||
}
|
||||
|
||||
String world = progress.worldName() == null ? "?" : progress.worldName();
|
||||
sender().sendMessage(C.GREEN + "Pregen " + C.GOLD + world + C.GREEN + ": " + C.GOLD + Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks())
|
||||
+ C.GREEN + " (" + C.GOLD + String.format("%.1f", progress.percent()) + "%" + C.GREEN + ")"
|
||||
+ (progress.paused() ? C.YELLOW + " PAUSED" : ""));
|
||||
sender().sendMessage(C.GREEN + "Speed: " + C.GOLD + Form.f((int) progress.chunksPerSecond()) + "/s" + C.GREEN
|
||||
+ " ETA: " + C.GOLD + Form.duration(progress.eta(), 2) + C.GREEN
|
||||
+ " Elapsed: " + C.GOLD + Form.duration(progress.elapsed(), 2) + C.GREEN
|
||||
+ " Method: " + C.GOLD + progress.method()
|
||||
+ (progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""));
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureCaptureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureIndexService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)")
|
||||
public class CommandStructure implements DirectorExecutor {
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
|
||||
public void list(
|
||||
@Param(description = "The dimension whose pack to index", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
File file = StructureIndexService.write(data);
|
||||
sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath());
|
||||
}
|
||||
|
||||
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
|
||||
public void importAll(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
StructureCaptureImporter.Report captured = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
|
||||
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
|
||||
sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect.");
|
||||
}
|
||||
|
||||
@Director(name = "capture", description = "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import.", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
|
||||
public void capture(
|
||||
@Param(description = "The dimension whose pack to capture into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
|
||||
}
|
||||
|
||||
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Search radius in chunks around the origin (larger is much slower)", defaultValue = "48")
|
||||
int radius
|
||||
) {
|
||||
World world = resolveIrisWorld(dimension);
|
||||
if (world == null) {
|
||||
sender().sendMessage(C.RED + "No loaded Iris world found for " + dimension.getLoadKey() + ". Join or create one first (the search runs against a live world).");
|
||||
return;
|
||||
}
|
||||
boolean senderIsPlayer = sender() != null && sender().isPlayer();
|
||||
Location center = senderIsPlayer && player().getWorld() == world
|
||||
? player().getLocation()
|
||||
: world.getSpawnLocation();
|
||||
int searchRadius = Math.max(1, Math.min(radius, 1000));
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
Engine engine = access == null ? null : access.getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "The selected Iris world has no active generator engine.");
|
||||
return;
|
||||
}
|
||||
KList<String> structureKeys = new KList<>();
|
||||
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
|
||||
for (Structure structure : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(structure);
|
||||
if (key != null) {
|
||||
structureKeys.add(key.toString());
|
||||
}
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = senderIsPlayer ? player() : null;
|
||||
commandSender.sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName()
|
||||
+ C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ()
|
||||
+ " within " + searchRadius + " chunks...");
|
||||
int centerX = center.getBlockX();
|
||||
int centerZ = center.getBlockZ();
|
||||
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
|
||||
}
|
||||
|
||||
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
|
||||
int searchRadius, VolmitSender commandSender, Player target) {
|
||||
KList<String> messages = new KList<>();
|
||||
Map<String, IrisNativeStructureDecision> decisions = new HashMap<>(structureKeys.size());
|
||||
boolean requiresNativeReachability = false;
|
||||
for (String keyName : structureKeys) {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false);
|
||||
decisions.put(keyName, decision);
|
||||
requiresNativeReachability |= decision.generate();
|
||||
}
|
||||
Set<String> reachable = Set.of();
|
||||
if (requiresNativeReachability) {
|
||||
try {
|
||||
reachable = StructureReachability.reachableKeys(engine);
|
||||
} catch (Throwable error) {
|
||||
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
int located = 0;
|
||||
int nativeEligible = 0;
|
||||
int disabled = 0;
|
||||
int unreachable = 0;
|
||||
int searchLimited = 0;
|
||||
int errors = 0;
|
||||
for (String keyName : structureKeys) {
|
||||
IrisNativeStructureDecision decision = decisions.get(keyName);
|
||||
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
searchLimited++;
|
||||
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
|
||||
+ ": unable to complete the density search before its safety limit was reached");
|
||||
continue;
|
||||
}
|
||||
if (!result.found()) {
|
||||
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
located++;
|
||||
messages.add(C.AQUA + "[iris] " + C.WHITE + keyName + C.GREEN + " @ "
|
||||
+ result.originX() + "," + result.baseY() + "," + result.originZ());
|
||||
} catch (Throwable error) {
|
||||
errors++;
|
||||
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!decision.generate()) {
|
||||
disabled++;
|
||||
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
|
||||
unreachable++;
|
||||
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
|
||||
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
|
||||
continue;
|
||||
}
|
||||
nativeEligible++;
|
||||
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
|
||||
}
|
||||
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
|
||||
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
|
||||
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
|
||||
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
|
||||
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
|
||||
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
}
|
||||
|
||||
private void sendVerificationMessages(VolmitSender commandSender, Player target,
|
||||
KList<String> messages) {
|
||||
Runnable send = () -> {
|
||||
for (String message : messages) {
|
||||
commandSender.sendMessage(message);
|
||||
}
|
||||
};
|
||||
if (target != null) {
|
||||
J.runEntity(target, send);
|
||||
} else {
|
||||
J.s(send);
|
||||
}
|
||||
}
|
||||
|
||||
private World resolveIrisWorld(IrisDimension dimension) {
|
||||
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
|
||||
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
|
||||
return player().getWorld();
|
||||
}
|
||||
}
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(w)) {
|
||||
continue;
|
||||
}
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(w);
|
||||
if (matchesDimension(gen, dimension.getLoadKey())) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
|
||||
return dimensionKey != null
|
||||
&& generator != null
|
||||
&& generator.getEngine() != null
|
||||
&& generator.getEngine().getDimension() != null
|
||||
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
|
||||
public void info(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to inspect")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = data.load(IrisStructure.class, structure, false);
|
||||
if (s == null) {
|
||||
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
|
||||
return;
|
||||
}
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, s, new IrisPosition(0, 64, 0));
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
|
||||
return;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
minX = Math.min(minX, p.getMinX());
|
||||
minZ = Math.min(minZ, p.getMinZ());
|
||||
maxX = Math.max(maxX, p.getMaxX());
|
||||
maxZ = Math.max(maxZ, p.getMaxZ());
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)");
|
||||
}
|
||||
|
||||
@Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void place(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to place")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = data.load(IrisStructure.class, structure, false);
|
||||
if (s == null) {
|
||||
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
|
||||
return;
|
||||
}
|
||||
Location loc = player().getLocation();
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
|
||||
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces");
|
||||
return;
|
||||
}
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
||||
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
config.setRotation(p.getRotation());
|
||||
config.getPlace().add(p.getObject().getLoadKey());
|
||||
if (!s.getEdit().isEmpty()) {
|
||||
config.setEdit(s.getEdit());
|
||||
}
|
||||
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location.");
|
||||
}
|
||||
|
||||
}
|
||||
+261
-306
@@ -16,54 +16,74 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.gui.NoiseExplorerGUI;
|
||||
import com.volmit.iris.core.gui.VisionGUI;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.project.IrisProject;
|
||||
import com.volmit.iris.core.service.ConversionSVC;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.*;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.collection.KSet;
|
||||
import com.volmit.iris.util.decree.DecreeContext;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.function.Function2;
|
||||
import com.volmit.iris.util.function.NoiseProvider;
|
||||
import com.volmit.iris.util.interpolation.InterpolationMethod;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONArray;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.mantle.MantleChunk;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.math.Position2;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.math.Spiraler;
|
||||
import com.volmit.iris.util.noise.CNG;
|
||||
import com.volmit.iris.util.parallel.MultiBurst;
|
||||
import com.volmit.iris.util.parallel.SyncExecutor;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import com.volmit.iris.util.scheduling.O;
|
||||
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
||||
import com.volmit.iris.util.scheduling.jobs.ParallelRadiusJob;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.*;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.service.BoardSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.FeatureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.util.BlockVector;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
@@ -74,17 +94,13 @@ import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
|
||||
public class CommandStudio implements DecreeExecutor {
|
||||
private CommandFind find;
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
@@ -92,24 +108,9 @@ public class CommandStudio implements DecreeExecutor {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
//TODO fix pack trimming
|
||||
@Decree(description = "Download a project.", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||
String pack,
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
|
||||
String branch,
|
||||
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||
//boolean trim,
|
||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||
boolean overwrite
|
||||
) {
|
||||
new CommandIris().download(pack, branch, overwrite);
|
||||
}
|
||||
|
||||
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
@Director(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim")
|
||||
@Param(description = "The dimension pack to open a studio for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed) {
|
||||
@@ -117,185 +118,155 @@ public class CommandStudio implements DecreeExecutor {
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
||||
@Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH)
|
||||
public void importvanilla(
|
||||
@Param(description = "The dimension pack to import vanilla content into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "How many variants to capture per tree/object feature", defaultValue = "3")
|
||||
int variants,
|
||||
@Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", defaultValue = "true")
|
||||
boolean structures
|
||||
) {
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Provide a dimension pack: /iris std importvanilla pack=<dimension>");
|
||||
return;
|
||||
}
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GREEN + "Importing vanilla content into " + C.WHITE + dimension.getLoadKey() + C.GREEN + "...");
|
||||
|
||||
FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender);
|
||||
int imported = features.imported();
|
||||
int failed = features.failed();
|
||||
|
||||
if (structures) {
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender);
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
imported += jigsaws.imported() + templates.imported() + groups.imported();
|
||||
failed += jigsaws.failed() + templates.failed() + groups.failed();
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "importvanilla complete: " + C.WHITE + imported + C.GREEN + " objects/structures written, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender.sendMessage(C.GRAY + "Trees/objects are under objects/vanilla/...; reference them from biome object placements.");
|
||||
}
|
||||
|
||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim")
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack");
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Close an open studio project", aliases = {"x", "c"}, sync = true)
|
||||
@Director(description = "Close an open studio project", aliases = {"x"}, sync = true)
|
||||
public void close() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No open studio projects.");
|
||||
commandSender.sendMessage(C.RED + "No open studio projects.");
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.service(StudioSVC.class).close();
|
||||
sender().sendMessage(C.GREEN + "Project Closed.");
|
||||
commandSender.sendMessage(C.YELLOW + "Closing studio...");
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.failureCause() != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.startupCleanupQueued()) {
|
||||
commandSender.sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.GREEN + "Studio closed.");
|
||||
}));
|
||||
}
|
||||
|
||||
@Decree(description = "Create a new studio project", aliases = "+", sync = true)
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.")
|
||||
String name,
|
||||
@Param(description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", contextual = true)
|
||||
IrisDimension template) {
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), name, template.getLoadKey());
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), name);
|
||||
@Director(description = "Toggle your Studio debug scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
|
||||
public void scoreboard() {
|
||||
Player target = player();
|
||||
VolmitSender commandSender = sender();
|
||||
if (!J.runEntity(target, () -> {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld());
|
||||
if (generator == null || !generator.isStudio()) {
|
||||
commandSender.sendMessage(C.RED + "You must be in a Studio world to toggle the debug scoreboard.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean visible = Iris.service(BoardSVC.class).toggle(target);
|
||||
commandSender.sendMessage((visible ? C.GREEN : C.YELLOW)
|
||||
+ "Studio debug scoreboard " + (visible ? "enabled." : "disabled."));
|
||||
})) {
|
||||
commandSender.sendMessage(C.RED + "Could not update the Studio debug scoreboard right now.");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Get the version of a pack")
|
||||
@Director(description = "Create a new studio project", aliases = "+", sync = true)
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.", defaultValue = "studio")
|
||||
String name,
|
||||
@Param(
|
||||
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.",
|
||||
contextual = true,
|
||||
customHandler = NullableDimensionHandler.class
|
||||
)
|
||||
IrisDimension template) {
|
||||
String projectName = name;
|
||||
if (name.equals("studio")) {
|
||||
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||
int suffix = 2;
|
||||
while (new File(workspace, projectName).exists()) {
|
||||
projectName = "studio" + suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), projectName, template.getLoadKey());
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), projectName);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Get the version of a pack")
|
||||
public void version(
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true)
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
||||
}
|
||||
|
||||
@Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER)
|
||||
public void regen(
|
||||
@Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5")
|
||||
int radius
|
||||
) {
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris World to use regen!");
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
var loc = player().getLocation().clone();
|
||||
|
||||
J.a(() -> {
|
||||
PlatformChunkGenerator plat = IrisToolbelt.access(world);
|
||||
Engine engine = plat.getEngine();
|
||||
DecreeContext.touch(sender);
|
||||
try (SyncExecutor executor = new SyncExecutor(20);
|
||||
var service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
|
||||
) {
|
||||
int x = loc.getBlockX() >> 4;
|
||||
int z = loc.getBlockZ() >> 4;
|
||||
|
||||
int rad = engine.getMantle().getRadius();
|
||||
var mantle = engine.getMantle().getMantle();
|
||||
var chunkMap = new KMap<Position2, MantleChunk>();
|
||||
ParallelRadiusJob prep = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
|
||||
@Override
|
||||
protected void execute(int rX, int rZ) {
|
||||
if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) {
|
||||
mantle.deleteChunk(rX + x, rZ + z);
|
||||
return;
|
||||
}
|
||||
rX += x;
|
||||
rZ += z;
|
||||
chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ));
|
||||
mantle.deleteChunk(rX, rZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Preparing Mantle";
|
||||
}
|
||||
}.retarget(radius + rad, 0, 0);
|
||||
CountDownLatch pLatch = new CountDownLatch(1);
|
||||
prep.execute(sender(), pLatch::countDown);
|
||||
pLatch.await();
|
||||
|
||||
|
||||
ParallelRadiusJob job = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
|
||||
@Override
|
||||
protected void execute(int x, int z) {
|
||||
plat.injectChunkReplacement(world, x, z, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Regenerating";
|
||||
}
|
||||
}.retarget(radius, x, z);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
job.execute(sender(), latch::countDown);
|
||||
latch.await();
|
||||
|
||||
chunkMap.forEach((pos, chunk) ->
|
||||
mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk));
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage("Error while regenerating chunks");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
DecreeContext.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Decree(description = "Convert objects in the \"convert\" folder")
|
||||
public void convert() {
|
||||
Iris.service(ConversionSVC.class).check(sender());
|
||||
//IrisConverter.convertSchematics(sender());
|
||||
}
|
||||
|
||||
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
|
||||
public void execute(
|
||||
@Param(description = "The script to run")
|
||||
IrisScript script
|
||||
) {
|
||||
engine().getExecution().execute(script.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
|
||||
public void noise() {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||
NoiseExplorerGUI.launch();
|
||||
}
|
||||
|
||||
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
|
||||
public void charge() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!");
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Charging spawners!");
|
||||
engine().getWorldManager().chargeEnergy();
|
||||
}
|
||||
|
||||
@Decree(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"})
|
||||
public void explore(
|
||||
@Param(description = "The generator to explore", contextual = true)
|
||||
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap"})
|
||||
public void noise(
|
||||
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
|
||||
IrisGenerator generator,
|
||||
@Param(description = "The seed to generate with", defaultValue = "12345")
|
||||
@Param(description = "The seed to preview the generator with", defaultValue = "12345")
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||
|
||||
Supplier<Function2<Double, Double, Double>> l = () -> {
|
||||
|
||||
if (generator == null) {
|
||||
return (x, z) -> 0D;
|
||||
}
|
||||
|
||||
return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
||||
};
|
||||
NoiseExplorerGUI.launch(l, "Custom Generator");
|
||||
}
|
||||
|
||||
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
|
||||
public void hotload() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world open!");
|
||||
if (generator == null) {
|
||||
NoiseExplorerGUI.launch();
|
||||
return;
|
||||
}
|
||||
Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload();
|
||||
sender().sendMessage(C.GREEN + "Hotloaded");
|
||||
|
||||
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
||||
NoiseExplorerGUI.launch(supplier, "Custom Generator");
|
||||
}
|
||||
|
||||
@Decree(description = "Show loot if a chest were right here", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void loot(
|
||||
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
|
||||
boolean fast,
|
||||
@@ -304,11 +275,11 @@ public class CommandStudio implements DecreeExecutor {
|
||||
) {
|
||||
if (noStudio()) return;
|
||||
|
||||
KList<IrisLootTable> tables = engine().getLootTables(RNG.r, player().getLocation().getBlock());
|
||||
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock());
|
||||
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
||||
|
||||
try {
|
||||
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
||||
EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
||||
@@ -323,10 +294,10 @@ public class CommandStudio implements DecreeExecutor {
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
|
||||
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
|
||||
ta.set(J.sr(() ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
Bukkit.getScheduler().cancelTask(ta.get());
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(C.GREEN + "Opened inventory!");
|
||||
return;
|
||||
}
|
||||
@@ -335,14 +306,14 @@ public class CommandStudio implements DecreeExecutor {
|
||||
inv.clear();
|
||||
}
|
||||
|
||||
engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
|
||||
}, 0, fast ? 5 : 35));
|
||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
|
||||
}, fast ? 5 : 35));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
||||
player().openInventory(inv);
|
||||
}
|
||||
|
||||
@Decree(description = "Calculate the chance for each region to generate", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "Calculate the chance for each region to generate", origin = DirectorOrigin.PLAYER)
|
||||
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
|
||||
var engine = engine();
|
||||
if (engine == null) {
|
||||
@@ -379,73 +350,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
@Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER)
|
||||
public void distances(@Param(description = "The radius in chunks") int radius) {
|
||||
var engine = engine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "Only works in an Iris world!");
|
||||
return;
|
||||
}
|
||||
var sender = sender();
|
||||
int d = radius * 2;
|
||||
KMap<String, KList<Position2>> data = new KMap<>();
|
||||
var multiBurst = new MultiBurst("Distance Sampler");
|
||||
var executor = multiBurst.burst(radius * radius);
|
||||
|
||||
sender.sendMessage(C.GRAY + "Generating data...");
|
||||
var loc = player().getLocation();
|
||||
int totalTasks = d * d;
|
||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||
int c = J.ar(() -> {
|
||||
sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures");
|
||||
}, 0);
|
||||
|
||||
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||
var struct = engine.getStructureAt(x, z);
|
||||
if (struct != null) {
|
||||
data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z));
|
||||
}
|
||||
completedTasks.incrementAndGet();
|
||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||
|
||||
executor.complete();
|
||||
multiBurst.close();
|
||||
J.car(c);
|
||||
|
||||
for (var key : data.keySet()) {
|
||||
var list = data.get(key);
|
||||
KList<Long> distances = new KList<>(list.size() - 1);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
var pos = list.get(i);
|
||||
double dist = Integer.MAX_VALUE;
|
||||
for (var p : list) {
|
||||
if (p.equals(pos)) continue;
|
||||
dist = Math.min(dist, Math.sqrt(Math.pow(pos.getX() - p.getX(), 2) + Math.pow(pos.getZ() - p.getZ(), 2)));
|
||||
}
|
||||
if (dist == Integer.MAX_VALUE) continue;
|
||||
distances.add(Math.round(dist * 16));
|
||||
}
|
||||
long[] array = new long[distances.size()];
|
||||
for (int i = 0; i < distances.size(); i++) {
|
||||
array[i] = distances.get(i);
|
||||
}
|
||||
Arrays.sort(array);
|
||||
long min = array.length > 0 ? array[0] : 0;
|
||||
long max = array.length > 0 ? array[array.length - 1] : 0;
|
||||
long sum = Arrays.stream(array).sum();
|
||||
long avg = array.length > 0 ? Math.round(sum / (double) array.length) : 0;
|
||||
String msg = "%s: %s => min: %s/max: %s -> avg: %s".formatted(key, list.size(), min, max, avg);
|
||||
sender.sendMessage(msg);
|
||||
}
|
||||
if (data.isEmpty()) {
|
||||
sender.sendMessage(C.RED + "No data found!");
|
||||
} else {
|
||||
sender.sendMessage(C.GREEN + "Done!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Decree(description = "Render a world map (External GUI)", aliases = "render")
|
||||
@Director(description = "Render a world map (External GUI)", aliases = "render")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||
World world
|
||||
@@ -457,13 +362,13 @@ public class CommandStudio implements DecreeExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
|
||||
sender().sendMessage(C.GREEN + "Opening map!");
|
||||
}
|
||||
|
||||
@Decree(description = "Package a dimension into a compressed format", aliases = "package")
|
||||
@Director(description = "Package a dimension into a compressed format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default")
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@@ -473,9 +378,9 @@ public class CommandStudio implements DecreeExecutor {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Decree(description = "Profiles the performance of a dimension", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "Profiles the performance of a dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
|
||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
// Todo: Make this more accurate
|
||||
@@ -483,6 +388,14 @@ public class CommandStudio implements DecreeExecutor {
|
||||
File report = Iris.instance.getDataFile("profile.txt");
|
||||
IrisProject project = new IrisProject(pack);
|
||||
IrisData data = IrisData.get(pack);
|
||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
||||
|
||||
if (activeEngine != null) {
|
||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
||||
} else {
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
}
|
||||
|
||||
KList<String> fileText = new KList<>();
|
||||
|
||||
@@ -662,7 +575,49 @@ public class CommandStudio implements DecreeExecutor {
|
||||
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
||||
}
|
||||
|
||||
@Decree(description = "Spawn an Iris entity", aliases = "summon", origin = DecreeOrigin.PLAYER)
|
||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (studioService != null && studioService.isProjectOpen()) {
|
||||
IrisProject activeProject = studioService.getActiveProject();
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (isGeneratorDimension(activeProvider, dimension)) {
|
||||
return activeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sender().isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Player player = sender().player();
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
|
||||
if (isGeneratorDimension(worldAccess, dimension)) {
|
||||
return worldAccess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
|
||||
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisDimension engineDimension = generator.getEngine().getDimension();
|
||||
if (engineDimension == null || engineDimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
|
||||
public void spawn(
|
||||
@Param(description = "The entity to spawn")
|
||||
IrisEntity entity,
|
||||
@@ -675,7 +630,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
@Decree(description = "Teleport to the active studio world", aliases = "stp", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
@Director(description = "Teleport to the active studio world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
@@ -688,19 +643,19 @@ public class CommandStudio implements DecreeExecutor {
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
PaperLib.teleportAsync(player(), Iris.service(StudioSVC.class)
|
||||
Player player = player();
|
||||
IrisWorld studioWorld = Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
.getWorld();
|
||||
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
|
||||
.thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Decree(description = "Update your dimension projects VSCode workspace")
|
||||
@Director(description = "Update your dimension projects VSCode workspace")
|
||||
public void update(
|
||||
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default")
|
||||
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "...");
|
||||
@@ -711,7 +666,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(aliases = "find-objects", description = "Get information about nearby structures")
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks")
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
@@ -748,7 +703,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||
pw.println("MC Version: " + Bukkit.getVersion());
|
||||
pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!"));
|
||||
pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!"));
|
||||
pw.println("Report Captured At: " + new Date());
|
||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||
|
||||
@@ -857,7 +812,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
BlockVector sz = IrisObject.sampleSize(ff);
|
||||
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
@@ -894,7 +849,7 @@ public class CommandStudio implements DecreeExecutor {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
if (!IrisToolbelt.isStudio(world())) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
+76
-38
@@ -16,35 +16,37 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.edit.BlockSignal;
|
||||
import com.volmit.iris.core.nms.INMS;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisRegion;
|
||||
import com.volmit.iris.util.data.B;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.matter.MatterMarker;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.edit.BlockSignal;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
|
||||
public class CommandWhat implements DecreeExecutor {
|
||||
@Decree(description = "What is in my hand?", origin = DecreeOrigin.PLAYER)
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER)
|
||||
public void hand() {
|
||||
try {
|
||||
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
|
||||
@@ -65,17 +67,21 @@ public class CommandWhat implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "What biome am i in?", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "What biome am i in?", origin = DirectorOrigin.PLAYER)
|
||||
public void biome() {
|
||||
try {
|
||||
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
|
||||
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + b.getDerivative().name() + ")");
|
||||
Biome derivative = b.getDerivative();
|
||||
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
|
||||
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + (derivativeKey == null ? "unregistered" : derivativeKey.getKey()) + ")");
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage("Non-Iris Biome: " + player().getLocation().getBlock().getBiome().name());
|
||||
Biome biome = player().getLocation().getBlock().getBiome();
|
||||
NamespacedKey key = resolveBiomeKey(biome);
|
||||
sender().sendMessage("Non-Iris Biome: " + (key == null ? "unregistered" : key));
|
||||
|
||||
if (player().getLocation().getBlock().getBiome().equals(Biome.CUSTOM)) {
|
||||
if (key == null || key.getKey().equals("custom")) {
|
||||
try {
|
||||
sender().sendMessage("Data Pack Biome: " + INMS.get().getTrueBiomeBaseKey(player().getLocation()) + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())) + ")");
|
||||
} catch (Throwable ee) {
|
||||
@@ -85,11 +91,11 @@ public class CommandWhat implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "What region am i in?", origin = DirectorOrigin.PLAYER)
|
||||
public void region() {
|
||||
try {
|
||||
Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
|
||||
IrisRegion r = engine().getRegion(chunk);
|
||||
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
|
||||
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
|
||||
|
||||
} catch (Throwable e) {
|
||||
@@ -98,7 +104,7 @@ public class CommandWhat implements DecreeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "What block am i looking at?", origin = DirectorOrigin.PLAYER)
|
||||
public void block() {
|
||||
BlockData bd;
|
||||
try {
|
||||
@@ -113,37 +119,37 @@ public class CommandWhat implements DecreeExecutor {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
|
||||
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
|
||||
|
||||
if (B.isStorage(bd)) {
|
||||
if (BukkitBlockResolution.isStorage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)");
|
||||
}
|
||||
|
||||
if (B.isLit(bd)) {
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)");
|
||||
}
|
||||
|
||||
if (B.isFoliage(bd)) {
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Foliage Block");
|
||||
}
|
||||
|
||||
if (B.isDecorant(bd)) {
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Decorant Block");
|
||||
}
|
||||
|
||||
if (B.isFluid(bd)) {
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Fluid Block");
|
||||
}
|
||||
|
||||
if (B.isFoliagePlantable(bd)) {
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Plantable Foliage Block");
|
||||
}
|
||||
|
||||
if (B.isSolid(bd)) {
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Solid Block");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Show markers in chunk", origin = DecreeOrigin.PLAYER)
|
||||
@Director(description = "Show markers in chunk", origin = DirectorOrigin.PLAYER)
|
||||
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) {
|
||||
Chunk c = player().getLocation().getChunk();
|
||||
|
||||
@@ -154,8 +160,8 @@ public class CommandWhat implements DecreeExecutor {
|
||||
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
|
||||
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
|
||||
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
|
||||
.convert((i) -> i.toLocation(c.getWorld())).forEach((i) -> {
|
||||
J.s(() -> BlockSignal.of(i.getBlock(), 100));
|
||||
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
|
||||
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
|
||||
v.incrementAndGet();
|
||||
});
|
||||
}
|
||||
@@ -166,4 +172,36 @@ public class CommandWhat implements DecreeExecutor {
|
||||
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||
}
|
||||
}
|
||||
|
||||
private NamespacedKey resolveBiomeKey(Biome biome) {
|
||||
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
|
||||
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
|
||||
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyValue = invokeNoThrow(biome, "getKey");
|
||||
if (keyValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object invokeNoThrow(Biome biome, String methodName) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = biome.getClass().getMethod(methodName);
|
||||
return method.invoke(biome);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
+8
-6
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import com.volmit.iris.util.math.M;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -51,10 +51,13 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return M.ms();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
world.setBiome(x, z, b);
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,9 +70,8 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
return world.getBiome(x, z);
|
||||
return world.getBiome(x, world.getMinHeight(), z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
|
||||
|
||||
public static void install() {
|
||||
GuiHost.set(new BukkitGuiHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine findActiveEngine() {
|
||||
try {
|
||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
return access.getEngine();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerHotloadHook(Runnable onHotload) {
|
||||
Listener listener = new HotloadListener(onHotload);
|
||||
hotloadHooks.put(onHotload, listener);
|
||||
Iris.instance.registerListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHotloadHook(Runnable onHotload) {
|
||||
Listener listener = hotloadHooks.remove(onHotload);
|
||||
if (listener != null) {
|
||||
Iris.instance.unregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiOverlay overlayFor(Engine engine) {
|
||||
return engine == null ? null : new BukkitVisionOverlay(engine);
|
||||
}
|
||||
|
||||
private static final class HotloadListener implements Listener {
|
||||
private final Runnable onHotload;
|
||||
|
||||
private HotloadListener(Runnable onHotload) {
|
||||
this.onHotload = onHotload;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent event) {
|
||||
onHotload.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final Engine engine;
|
||||
|
||||
public BukkitVisionOverlay(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : BukkitWorldBinding.players(world)) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
|
||||
entity.getHealth(), maxHealth));
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasPlatformWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = BukkitWorldBinding.players(world);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player player = players.get(0);
|
||||
int xx = (int) worldX;
|
||||
int zz = (int) worldZ;
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -16,12 +16,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.link;
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
@@ -73,15 +74,15 @@ public class IrisPapiExpansion extends PlaceholderExpansion {
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("region_name")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getName();
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getName();
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("region_id")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadKey();
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadKey();
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("region_file")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadFile().getPath();
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("terrain_slope")) {
|
||||
if (a != null) {
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
DatapackIngestService.refreshWorkspace(engine.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadDatapacks(Engine engine) {
|
||||
synchronized (ServerConfigurator.class) {
|
||||
ServerConfigurator.installDataPacks(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireHotloadEvent(Engine engine) {
|
||||
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible(
|
||||
"Bukkit Studio world '" + engine.getWorld().name() + "'",
|
||||
engine.getDimension(),
|
||||
replacement,
|
||||
"iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPregeneratorActive(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownPregenerator(Engine engine) {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisableChunkContextCache(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
|
||||
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleCleanup(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
return world != null
|
||||
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
|
||||
IrisWorld irisWorld = engine.getWorld();
|
||||
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(irisWorld);
|
||||
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBypassMantleStages(Engine engine) {
|
||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.commands.CommandIris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.iris.util.common.director.DirectorContextHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorSystem;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
|
||||
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorSender;
|
||||
import art.arcane.volmlib.util.director.visual.DirectorVisualCommand;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
private static final String ROOT_PERMISSION = "iris.all";
|
||||
|
||||
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<DirectorVisualCommand> helpCache = new AtomicCache<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
PluginCommand command = findBukkitCommand();
|
||||
if (command != null) {
|
||||
command.setExecutor(this);
|
||||
command.setTabCompleter(this);
|
||||
} else {
|
||||
registerPaperCommand();
|
||||
}
|
||||
J.a(this::getDirector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
public DirectorRuntimeEngine getDirector() {
|
||||
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
|
||||
new CommandIris(),
|
||||
null,
|
||||
buildDirectorContexts(),
|
||||
this::dispatchDirector,
|
||||
this,
|
||||
DirectorSystem.handlers
|
||||
));
|
||||
}
|
||||
|
||||
private DirectorContextRegistry buildDirectorContexts() {
|
||||
DirectorContextRegistry contexts = new DirectorContextRegistry();
|
||||
|
||||
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
|
||||
registerContextHandler(contexts, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
|
||||
contexts.register((Class) type, (invocation, map) -> {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
|
||||
if (mode == DirectorExecutionMode.SYNC) {
|
||||
J.s(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
DirectorContext.touch(new VolmitSender(sender.sender()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
DirectorContext.remove();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return tabCompleteRoot(sender, alias, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
executeRoot(sender, label, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
if (!sender.hasPermission(ROOT_PERMISSION)) {
|
||||
sender.sendMessage("You lack the Permission '" + ROOT_PERMISSION + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
J.aBukkit(() -> executeCommand(sender, label, args));
|
||||
}
|
||||
|
||||
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
List<String> suggestions = runDirectorTab(sender, alias, args);
|
||||
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private PluginCommand findBukkitCommand() {
|
||||
try {
|
||||
return Iris.instance.getCommand(ROOT_COMMAND);
|
||||
} catch (UnsupportedOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPaperCommand() {
|
||||
try {
|
||||
Class<?> registrarType = Class.forName(
|
||||
"art.arcane.iris.core.service.PaperCommandRegistrar",
|
||||
true,
|
||||
getClass().getClassLoader()
|
||||
);
|
||||
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
|
||||
register.invoke(null, Iris.instance, this);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
throw new IllegalStateException("Paper command registration failed", cause);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException("Paper command registrar is unavailable", e);
|
||||
} catch (LinkageError e) {
|
||||
throw new IllegalStateException("Paper command APIs are unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCommand(CommandSender sender, String label, String[] args) {
|
||||
if (sendHelpIfRequested(sender, args)) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
DirectorExecutionResult result = runDirector(sender, label, args);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
playFailureSound(sender);
|
||||
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
|
||||
new VolmitSender(sender).sendMessage(C.RED + "Unknown Iris Command");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
|
||||
Optional<DirectorVisualCommand.HelpRequest> request = DirectorVisualCommand.resolveHelp(getHelpRoot(), Arrays.asList(args));
|
||||
if (request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VolmitSender volmitSender = new VolmitSender(sender);
|
||||
volmitSender.sendDirectorHelp(request.get().command(), request.get().page());
|
||||
return true;
|
||||
}
|
||||
|
||||
private DirectorVisualCommand getHelpRoot() {
|
||||
return helpCache.aquireNastyPrint(() -> DirectorVisualCommand.createRoot(getDirector()));
|
||||
}
|
||||
|
||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||
try {
|
||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return DirectorExecutionResult.notHandled();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try {
|
||||
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return List.of();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void playFailureSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
|
||||
}
|
||||
}
|
||||
|
||||
private void playSuccessSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
|
||||
}
|
||||
}
|
||||
|
||||
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
|
||||
try {
|
||||
player.playSound(player.getLocation(), a, av, ap);
|
||||
player.playSound(player.getLocation(), b, bv, bp);
|
||||
} catch (Throwable e) {
|
||||
if (SOUND_LOGGED.compareAndSet(false, true)) {
|
||||
try {
|
||||
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
|
||||
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
|
||||
@Override
|
||||
public String getName() {
|
||||
return sender.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayer() {
|
||||
return sender instanceof Player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
if (message != null && !message.trim().isEmpty()) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-10
@@ -16,33 +16,42 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.service;
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.edit.BlockEditor;
|
||||
import com.volmit.iris.core.edit.BukkitBlockEditor;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.plugin.IrisService;
|
||||
import org.bukkit.Bukkit;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.edit.BlockEditor;
|
||||
import art.arcane.iris.core.edit.BukkitBlockEditor;
|
||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
public class EditSVC implements IrisService {
|
||||
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
|
||||
private KMap<World, BlockEditor> editors;
|
||||
private int updateTaskId = -1;
|
||||
public static boolean deletingWorld = false;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.editors = new KMap<>();
|
||||
Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000);
|
||||
updateTaskId = J.sr(this::update, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (updateTaskId != -1) {
|
||||
J.csr(updateTaskId);
|
||||
updateTaskId = -1;
|
||||
}
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
flushNow();
|
||||
}
|
||||
|
||||
@@ -72,6 +81,9 @@ public class EditSVC implements IrisService {
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldUnloadEvent e) {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
|
||||
editors.remove(e.getWorld()).close();
|
||||
}
|
||||
@@ -79,6 +91,9 @@ public class EditSVC implements IrisService {
|
||||
|
||||
|
||||
public void update() {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
if (M.ms() - editors.get(i).last() > 1000) {
|
||||
editors.remove(i).close();
|
||||
@@ -87,12 +102,18 @@ public class EditSVC implements IrisService {
|
||||
}
|
||||
|
||||
public void flushNow() {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
editors.remove(i).close();
|
||||
}
|
||||
}
|
||||
|
||||
public BlockEditor open(World world) {
|
||||
if (editors == null) {
|
||||
editors = new KMap<>();
|
||||
}
|
||||
if (editors.containsKey(world)) {
|
||||
return editors.get(world);
|
||||
}
|
||||
+208
-30
@@ -1,21 +1,28 @@
|
||||
package com.volmit.iris.core.service;
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.loader.ResourceLoader;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.plugin.IrisService;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.Looper;
|
||||
import com.volmit.iris.util.stream.utility.CachedStream2D;
|
||||
import com.volmit.iris.util.stream.utility.CachedStream3D;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
|
||||
import lombok.Synchronized;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
@@ -24,7 +31,11 @@ import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -60,20 +71,81 @@ public class IrisEngineSVC implements IrisService {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
service.shutdown();
|
||||
updateTicker.interrupt();
|
||||
for (World world : worlds.keySet()) {
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(world);
|
||||
if (gen == null) continue;
|
||||
try {
|
||||
gen.close();
|
||||
} catch (Throwable t) {
|
||||
IrisLogging.reportError(t);
|
||||
}
|
||||
}
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
if (updateTicker != null) {
|
||||
updateTicker.interrupt();
|
||||
}
|
||||
worlds.keySet().forEach(this::remove);
|
||||
worlds.clear();
|
||||
}
|
||||
|
||||
public int getQueuedTectonicPlateCount() {
|
||||
return queuedTectonicPlates.get();
|
||||
}
|
||||
|
||||
public double getAverageIdleDuration() {
|
||||
double min = minIdleDuration.get();
|
||||
double max = maxIdleDuration.get();
|
||||
if (!Double.isFinite(min) || !Double.isFinite(max) || min < 0D || max < 0D) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
if (max < min) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return (min + max) / 2D;
|
||||
}
|
||||
|
||||
public double getBiomeCacheUsageRatio() {
|
||||
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
|
||||
if (preservation == null) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
double total = 0D;
|
||||
int count = 0;
|
||||
for (var cache : preservation.getCaches()) {
|
||||
if (!(cache instanceof CachedStream2D<?>) && !(cache instanceof CachedDoubleStream2D)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double usage = cache.getUsage();
|
||||
if (!Double.isFinite(usage)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
total += Math.max(0D, Math.min(1D, usage));
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
return total / count;
|
||||
}
|
||||
|
||||
public void engineStatus(VolmitSender sender) {
|
||||
long[] sizes = new long[4];
|
||||
long[] count = new long[4];
|
||||
|
||||
for (var cache : Iris.service(PreservationSVC.class).getCaches()) {
|
||||
for (var cache : IrisServices.get(PreservationSVC.class).getCaches()) {
|
||||
var type = switch (cache) {
|
||||
case ResourceLoader<?> ignored -> 0;
|
||||
case CachedStream2D<?> ignored -> 1;
|
||||
case CachedDoubleStream2D ignored -> 1;
|
||||
case CachedStream3D<?> ignored -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
@@ -123,7 +195,8 @@ public class IrisEngineSVC implements IrisService {
|
||||
}
|
||||
|
||||
private void add(World world) {
|
||||
var access = IrisToolbelt.access(world);
|
||||
if (world == null || worlds.containsKey(world)) return;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null) return;
|
||||
worlds.put(world, new Registered(world.getName(), access));
|
||||
}
|
||||
@@ -136,6 +209,10 @@ public class IrisEngineSVC implements IrisService {
|
||||
@Override
|
||||
protected long loop() {
|
||||
try {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
add(world);
|
||||
}
|
||||
|
||||
int queuedPlates = 0;
|
||||
int totalPlates = 0;
|
||||
long chunks = 0;
|
||||
@@ -184,6 +261,24 @@ public class IrisEngineSVC implements IrisService {
|
||||
updateTicker.start();
|
||||
}
|
||||
|
||||
private static boolean isMantleClosed(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.getCause();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean shouldSkipMantleReductionForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
|
||||
return maintenanceActive && !pregeneratorTargetsWorld;
|
||||
}
|
||||
|
||||
private final class Registered {
|
||||
private final String name;
|
||||
private final PlatformChunkGenerator access;
|
||||
@@ -214,14 +309,32 @@ public class IrisEngineSVC implements IrisService {
|
||||
if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) {
|
||||
trimmer = service.scheduleAtFixedRate(() -> {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine))
|
||||
if (engine == null
|
||||
|| engine.isClosed()
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
if (GoldenHashScanner.isScanActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
engine.getMantle().trim(tectonicLimit());
|
||||
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
|
||||
engine.getMantle().trim(0L, 0);
|
||||
} else {
|
||||
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("EngineSVC: Failed to trim for " + name);
|
||||
if (isMantleClosed(e)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("EngineSVC: Failed to trim for " + name);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS);
|
||||
@@ -230,18 +343,38 @@ public class IrisEngineSVC implements IrisService {
|
||||
if (unloader == null || unloader.isDone() || unloader.isCancelled()) {
|
||||
unloader = service.scheduleAtFixedRate(() -> {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine))
|
||||
if (engine == null
|
||||
|| engine.isClosed()
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (GoldenHashScanner.isScanActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long unloadStart = System.currentTimeMillis();
|
||||
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : tectonicLimit());
|
||||
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
|
||||
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine);
|
||||
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
|
||||
if (heapPressure && MantleHeapPressure.overPanicWater()) {
|
||||
MantleHeapPressure.requestPanicReclaim();
|
||||
}
|
||||
if (count > 0) {
|
||||
Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
|
||||
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("EngineSVC: Failed to unload for " + name);
|
||||
if (isMantleClosed(e)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("EngineSVC: Failed to unload for " + name);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS);
|
||||
@@ -252,6 +385,28 @@ public class IrisEngineSVC implements IrisService {
|
||||
return tectonicLimit.get() / Math.max(worlds.size(), 1);
|
||||
}
|
||||
|
||||
private boolean pregenTargets(Engine engine) {
|
||||
if (engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(Engine engine) {
|
||||
int limit = tectonicLimit();
|
||||
if (!pregenTargets(engine)) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
return Math.max(limit, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
|
||||
}
|
||||
|
||||
private long activeIdleDuration(Engine engine) {
|
||||
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private void close() {
|
||||
if (closed) return;
|
||||
@@ -273,5 +428,28 @@ public class IrisEngineSVC implements IrisService {
|
||||
if (closed) return null;
|
||||
return access.getEngine();
|
||||
}
|
||||
|
||||
private boolean shouldReduce(Engine engine) {
|
||||
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean shouldSkipForMaintenance(@Nullable World world) {
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
|
||||
if (!maintenanceActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
|
||||
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||
import art.arcane.volmlib.integration.IntegrationServiceContract;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||
new IntegrationProtocolVersion(1, 0),
|
||||
new IntegrationProtocolVersion(1, 1)
|
||||
);
|
||||
|
||||
private static final Set<String> CAPABILITIES = Set.of(
|
||||
"handshake",
|
||||
"heartbeat",
|
||||
"metrics",
|
||||
"iris-engine-metrics"
|
||||
);
|
||||
|
||||
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||
Iris.verbose("Integration provider registered for Iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginId() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationProtocolVersion> supportedProtocols() {
|
||||
return SUPPORTED_PROTOCOLS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> capabilities() {
|
||||
return CAPABILITIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationMetricDescriptor> metricDescriptors() {
|
||||
return IntegrationMetricSchema.descriptors().stream()
|
||||
.filter(descriptor -> descriptor.key().startsWith("iris."))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (request == null) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"missing request",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
|
||||
SUPPORTED_PROTOCOLS,
|
||||
request.supportedProtocols()
|
||||
);
|
||||
if (negotiated.isEmpty()) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"no-common-protocol",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
negotiatedProtocol = negotiated.get();
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
true,
|
||||
negotiatedProtocol,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"ok",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHeartbeat heartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
|
||||
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
|
||||
? IntegrationMetricSchema.irisKeys()
|
||||
: metricKeys;
|
||||
long now = System.currentTimeMillis();
|
||||
Map<String, IntegrationMetricSample> out = new HashMap<>();
|
||||
|
||||
for (String key : requested) {
|
||||
switch (key) {
|
||||
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
|
||||
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
|
||||
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
|
||||
default -> out.put(key, IntegrationMetricSample.unavailable(
|
||||
IntegrationMetricSchema.descriptor(key),
|
||||
"unsupported-key",
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
|
||||
|
||||
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
|
||||
|
||||
if (chunksPerSecond > 0D) {
|
||||
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
|
||||
}
|
||||
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService != null) {
|
||||
double idle = engineService.getAverageIdleDuration();
|
||||
if (idle > 0D && Double.isFinite(idle)) {
|
||||
return IntegrationMetricSample.available(descriptor, idle, now);
|
||||
}
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, 0D, now);
|
||||
}
|
||||
|
||||
private IntegrationMetricSample samplePregenQueueMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
|
||||
long totalQueue = 0L;
|
||||
boolean hasAnySource = false;
|
||||
|
||||
long pregenRemaining = PregeneratorJob.chunksRemaining();
|
||||
if (pregenRemaining >= 0L) {
|
||||
totalQueue += pregenRemaining;
|
||||
hasAnySource = true;
|
||||
}
|
||||
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService != null) {
|
||||
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
|
||||
hasAnySource = true;
|
||||
}
|
||||
|
||||
if (!hasAnySource) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, totalQueue, now);
|
||||
}
|
||||
|
||||
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService == null) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
|
||||
}
|
||||
|
||||
double ratio = engineService.getBiomeCacheUsageRatio();
|
||||
if (!Double.isFinite(ratio)) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.protocol.EngineResolver;
|
||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||
import art.arcane.iris.core.protocol.IrisServerTransport;
|
||||
import art.arcane.iris.core.protocol.IrisSession;
|
||||
import art.arcane.iris.core.protocol.IrisSessionRegistry;
|
||||
import art.arcane.iris.core.protocol.IrisVisionRequestService;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.plugin.messaging.Messenger;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport {
|
||||
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
|
||||
private static final int DIMENSION_SYNC_RETRY_TICKS = 20;
|
||||
private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15;
|
||||
|
||||
private IrisSessionRegistry registry;
|
||||
private IrisProtocolServer protocolServer;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Messenger messenger = Bukkit.getMessenger();
|
||||
messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
|
||||
messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
|
||||
registry = new IrisSessionRegistry();
|
||||
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
|
||||
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
|
||||
protocolServer.setEngineResolver(engineResolver);
|
||||
protocolServer.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, registry));
|
||||
IrisServices.register(IrisProtocolServer.class, protocolServer);
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
registry.register(new IrisSession(player.getUniqueId().toString(), this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
IrisServices.remove(IrisProtocolServer.class);
|
||||
Messenger messenger = Bukkit.getMessenger();
|
||||
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
|
||||
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current != null) {
|
||||
for (IrisSession session : current.all()) {
|
||||
current.unregister(session.id());
|
||||
}
|
||||
}
|
||||
registry = null;
|
||||
protocolServer = null;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this));
|
||||
syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
syncDimension(event.getPlayer(), 1);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
current.unregister(event.getPlayer().getUniqueId().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
|
||||
IrisProtocolServer current = protocolServer;
|
||||
if (current == null || !IrisProtocol.CHANNEL.equals(channel)) {
|
||||
return;
|
||||
}
|
||||
current.onClientFrame(player.getUniqueId().toString(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToClient(String sessionId, byte[] frame) {
|
||||
if (sessionId == null || frame == null) {
|
||||
return;
|
||||
}
|
||||
UUID playerId = parseUuid(sessionId);
|
||||
if (playerId == null) {
|
||||
return;
|
||||
}
|
||||
Player player = Bukkit.getPlayer(playerId);
|
||||
if (player == null || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
J.runEntity(player, () -> deliver(player, frame));
|
||||
}
|
||||
|
||||
private void deliver(Player player, byte[] frame) {
|
||||
if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) {
|
||||
return;
|
||||
}
|
||||
player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame);
|
||||
}
|
||||
|
||||
private void syncDimension(Player player, int attemptsLeft) {
|
||||
IrisProtocolServer protocol = protocolServer;
|
||||
IrisSessionRegistry current = registry;
|
||||
if (protocol == null || current == null || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
String sessionId = player.getUniqueId().toString();
|
||||
IrisSession session = current.get(sessionId);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (!session.isReady()) {
|
||||
if (attemptsLeft > 1) {
|
||||
J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
World world = player.getWorld();
|
||||
Engine engine = engineFor(world);
|
||||
if (engine != null) {
|
||||
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
|
||||
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
|
||||
return;
|
||||
}
|
||||
protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false);
|
||||
}
|
||||
|
||||
private static Engine resolveEngine(String sessionId) {
|
||||
UUID playerId = parseUuid(sessionId);
|
||||
if (playerId == null) {
|
||||
return null;
|
||||
}
|
||||
Player player = Bukkit.getPlayer(playerId);
|
||||
if (player == null || !player.isOnline()) {
|
||||
return null;
|
||||
}
|
||||
return engineFor(player.getWorld());
|
||||
}
|
||||
|
||||
private static Engine engineFor(World world) {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static UUID parseUuid(String sessionId) {
|
||||
try {
|
||||
return UUID.fromString(sessionId);
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String brand() {
|
||||
return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)";
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
final class PaperCommandRegistrar {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
|
||||
private PaperCommandRegistrar() {
|
||||
}
|
||||
|
||||
static void register(Iris plugin, CommandSVC commandService) {
|
||||
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
|
||||
event.registrar().register(
|
||||
ROOT_COMMAND,
|
||||
"Iris world generation command.",
|
||||
List.of("ir", "irs"),
|
||||
command(commandService)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static BasicCommand command(CommandSVC commandService) {
|
||||
return new PaperCommand(commandService);
|
||||
}
|
||||
|
||||
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
|
||||
@Override
|
||||
public void execute(CommandSourceStack source, String[] args) {
|
||||
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> suggest(CommandSourceStack source, String[] args) {
|
||||
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse(CommandSender sender) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-25
@@ -16,26 +16,33 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.service;
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.edit.DustRevealer;
|
||||
import com.volmit.iris.core.link.WorldEditLink;
|
||||
import com.volmit.iris.core.wand.WandSelection;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.data.Cuboid;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.matter.Matter;
|
||||
import com.volmit.iris.util.matter.WorldMatter;
|
||||
import com.volmit.iris.util.plugin.IrisService;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import com.volmit.iris.util.scheduling.SR;
|
||||
import com.volmit.iris.util.scheduling.jobs.Job;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.edit.DustRevealer;
|
||||
import art.arcane.iris.core.link.WorldEditLink;
|
||||
import art.arcane.iris.core.wand.WandSelection;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.util.project.matter.WorldMatter;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.SR;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.Job;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -51,12 +58,11 @@ import org.bukkit.util.Vector;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static com.volmit.iris.util.data.registry.Particles.CRIT_MAGIC;
|
||||
import static com.volmit.iris.util.data.registry.Particles.REDSTONE;
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.CRIT_MAGIC;
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
|
||||
|
||||
public class WandSVC implements IrisService {
|
||||
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
|
||||
@@ -206,7 +212,9 @@ public class WandSVC implements IrisService {
|
||||
if (f.length != 2) return null;
|
||||
String[] g = f[0].split("\\Q,\\E");
|
||||
if (g.length != 3) return null;
|
||||
return new Location(Bukkit.getWorld(f[1]), Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
|
||||
World world = WorldIdentity.resolve(f[1]).orElse(null);
|
||||
if (world == null) return null;
|
||||
return new Location(world, Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return null;
|
||||
@@ -224,7 +232,7 @@ public class WandSVC implements IrisService {
|
||||
return "<#>";
|
||||
}
|
||||
|
||||
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + loc.getWorld().getName();
|
||||
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,7 +536,7 @@ public class WandSVC implements IrisService {
|
||||
Location[] f = getCuboidFromItem(item);
|
||||
Location other = left ? f[1] : f[0];
|
||||
|
||||
if (other != null && !other.getWorld().getName().equals(a.getWorld().getName())) {
|
||||
if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) {
|
||||
other = null;
|
||||
}
|
||||
|
||||
+5
-5
@@ -16,18 +16,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.wand;
|
||||
package art.arcane.iris.core.wand;
|
||||
|
||||
import com.volmit.iris.util.data.Cuboid;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.Color;
|
||||
|
||||
import static com.volmit.iris.util.data.registry.Particles.REDSTONE;
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
|
||||
|
||||
public class WandSelection {
|
||||
private final Cuboid c;
|
||||
@@ -0,0 +1,72 @@
|
||||
name: ${name}
|
||||
version: ${version}
|
||||
main: ${main}
|
||||
bootstrapper: ${bootstrapper}
|
||||
folia-supported: true
|
||||
api-version: '${apiVersion}'
|
||||
load: STARTUP
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
dependencies:
|
||||
server:
|
||||
PlaceholderAPI:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
CraftEngine:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Nexo:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ItemsAdder:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
SCore:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ExecutableItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: false
|
||||
MythicLib:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MMOItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
eco:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
EcoItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicMobs:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicCrucible:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
KGenerators:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Multiverse-Core:
|
||||
load: AFTER
|
||||
required: false
|
||||
join-classpath: true
|
||||
WorldEdit:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
@@ -0,0 +1,28 @@
|
||||
name: ${name}
|
||||
version: ${version}
|
||||
main: ${main}
|
||||
folia-supported: true
|
||||
load: STARTUP
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
- CraftEngine
|
||||
- Nexo
|
||||
- ItemsAdder
|
||||
- SCore
|
||||
- ExecutableItems
|
||||
- MythicLib
|
||||
- MMOItems
|
||||
- eco
|
||||
- EcoItems
|
||||
- MythicMobs
|
||||
- MythicCrucible
|
||||
- KGenerators
|
||||
- WorldEdit
|
||||
loadbefore: [ Multiverse-Core ]
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
commands:
|
||||
iris:
|
||||
aliases: [ ir, irs ]
|
||||
api-version: '${apiVersion}'
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.core.service.CommandSVC;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDiagnosticsTest {
|
||||
@Test
|
||||
public void serviceInitializationSkipsPackageHelperClasses() throws Exception {
|
||||
Class<?> registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar");
|
||||
|
||||
assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class));
|
||||
assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportErrorWithContextPrintsFullStacktrace() {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
try {
|
||||
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("Runtime world creation failed."));
|
||||
assertTrue(text.contains("IllegalStateException"));
|
||||
assertTrue(text.contains("IllegalArgumentException"));
|
||||
assertTrue(text.contains("inner"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception {
|
||||
Path root = Files.createTempDirectory("iris-splash");
|
||||
try {
|
||||
Path validPack = root.resolve("overworld");
|
||||
Files.createDirectories(validPack.resolve("dimensions"));
|
||||
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
|
||||
|
||||
Files.createDirectories(root.resolve("datapack-imports"));
|
||||
|
||||
Path brokenPack = root.resolve("broken");
|
||||
Files.createDirectories(brokenPack.resolve("dimensions"));
|
||||
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
List<IrisSplashPackScanner.SplashPackMetadata> packs;
|
||||
try {
|
||||
packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError);
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
assertEquals(1, packs.size());
|
||||
assertEquals("overworld", packs.get(0).name());
|
||||
assertEquals("4000", packs.get(0).version());
|
||||
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
|
||||
assertTrue(text.contains("Json"));
|
||||
} finally {
|
||||
Files.walk(root)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.plugin.PluginLoadOrder;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperPluginMetadataTest {
|
||||
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
|
||||
"PlaceholderAPI",
|
||||
"CraftEngine",
|
||||
"Nexo",
|
||||
"ItemsAdder",
|
||||
"SCore",
|
||||
"ExecutableItems",
|
||||
"MythicLib",
|
||||
"MMOItems",
|
||||
"eco",
|
||||
"EcoItems",
|
||||
"MythicMobs",
|
||||
"MythicCrucible",
|
||||
"KGenerators",
|
||||
"Multiverse-Core",
|
||||
"WorldEdit"
|
||||
);
|
||||
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
|
||||
@Test
|
||||
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
||||
String metadata;
|
||||
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/paper-plugin.yml")) {
|
||||
assertNotNull(stream);
|
||||
metadata = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
|
||||
assertTrue(metadata.contains("folia-supported: true"));
|
||||
assertTrue(metadata.contains("load: STARTUP"));
|
||||
assertFalse(metadata.contains("commands:"));
|
||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
||||
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
|
||||
}
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitMetadataRetainsStartupCommandAndAliases() throws Exception {
|
||||
PluginDescriptionFile metadata;
|
||||
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/plugin.yml")) {
|
||||
assertNotNull(stream);
|
||||
metadata = new PluginDescriptionFile(stream);
|
||||
}
|
||||
|
||||
assertEquals(Iris.class.getName(), metadata.getMain());
|
||||
assertEquals(PluginLoadOrder.STARTUP, metadata.getLoad());
|
||||
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
||||
assertTrue(commands.containsKey("iris"));
|
||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
||||
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processedResourcesContainBothMetadataFormats() throws Exception {
|
||||
URL paperMetadata = PaperPluginMetadataTest.class.getResource("/paper-plugin.yml");
|
||||
assertNotNull(paperMetadata);
|
||||
assertEquals("file", paperMetadata.getProtocol());
|
||||
Path bukkitMetadata = Path.of(paperMetadata.toURI()).resolveSibling("plugin.yml");
|
||||
assertTrue(Files.isRegularFile(bukkitMetadata));
|
||||
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
|
||||
}
|
||||
|
||||
private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) {
|
||||
return " " + pluginId + ":\n"
|
||||
+ " load: " + loadOrder + "\n"
|
||||
+ " required: false\n"
|
||||
+ " join-classpath: " + joinClasspath + "\n";
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.commands.CommandStudio;
|
||||
import art.arcane.iris.core.tools.IrisCreator;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class StudioRuntimeCleanupTest {
|
||||
@Test
|
||||
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
|
||||
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
|
||||
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioCommandNoLongerExposesProfilecache() {
|
||||
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
|
||||
.anyMatch(method -> method.getName().equals("profilecache"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
|
||||
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
|
||||
.map(method -> method.getName().toLowerCase())
|
||||
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noisemapPrebakePipelineClassIsRemoved() {
|
||||
try {
|
||||
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStructureLocateCommandContractTest {
|
||||
@Test
|
||||
public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||
|
||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(source.contains("the density search safety limit was reached"));
|
||||
assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findRoutesOnlyExplicitNativeReplacementThroughIrisLocate() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||
int methodStart = source.indexOf("public void structure(");
|
||||
int methodEnd = source.indexOf("private static Structure resolveNativeStructure", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)");
|
||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution);
|
||||
int replacementCheck = method.indexOf(
|
||||
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
|
||||
int replacementLocate = method.indexOf("locateIrisStructure(e, structureKey, commandSender)", replacementCheck);
|
||||
int genericIrisLookup = method.indexOf(
|
||||
"nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", replacementLocate);
|
||||
int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", genericIrisLookup);
|
||||
assertTrue(nativeResolution >= 0);
|
||||
assertTrue(policyResolution > nativeResolution);
|
||||
assertTrue(replacementCheck > policyResolution);
|
||||
assertTrue(replacementLocate > replacementCheck);
|
||||
assertTrue(genericIrisLookup > replacementLocate);
|
||||
assertTrue(nativeLocate > policyResolution);
|
||||
assertTrue(method.contains("decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS"));
|
||||
assertFalse(method.contains("NativeStructureLocateCapability"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNativePolicyMessagesMatchModdedDiagnostics() {
|
||||
assertEquals(
|
||||
"Native structure minecraft:village_plains is disabled by this dimension's importedStructures settings.",
|
||||
NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
"minecraft:village_plains", NativeStructureGenerationStatus.DISABLED_BY_PACK));
|
||||
assertEquals(
|
||||
"Native structure minecraft:ancient_city is replaced by an Iris placement in this pack and locates through that explicit replacement.",
|
||||
NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
"minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS));
|
||||
assertEquals(
|
||||
"Native structure minecraft:stronghold generates natively.",
|
||||
NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
"minecraft:stronghold", NativeStructureGenerationStatus.GENERATE_NATIVE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
|
||||
|
||||
assertTrue(source.contains("[iris-search-limit]"));
|
||||
assertTrue(source.contains("density searches safety-limited"));
|
||||
assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureVerifyPartitionsPolicyBeforeNativeReachability() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
|
||||
int methodStart = source.indexOf("private void runVerification(");
|
||||
int methodEnd = source.indexOf("private void sendVerificationMessages(", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, keyName, false)");
|
||||
int nativeRequirement = method.indexOf("requiresNativeReachability |= decision.generate()", policyResolution);
|
||||
int reachabilityGuard = method.indexOf("if (requiresNativeReachability)", nativeRequirement);
|
||||
int reachabilityLookup = method.indexOf("StructureReachability.reachableKeys(engine)", reachabilityGuard);
|
||||
|
||||
assertTrue(policyResolution >= 0);
|
||||
assertTrue(nativeRequirement > policyResolution);
|
||||
assertTrue(reachabilityGuard > nativeRequirement);
|
||||
assertTrue(reachabilityLookup > reachabilityGuard);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StudioScoreboardCommandTest {
|
||||
@Test
|
||||
public void scoreboardCommandIsPlayerScopedAndEntityScheduled() throws NoSuchMethodException {
|
||||
Method command = CommandStudio.class.getDeclaredMethod("scoreboard");
|
||||
Director director = command.getAnnotation(Director.class);
|
||||
|
||||
assertEquals(DirectorOrigin.PLAYER, director.origin());
|
||||
assertFalse(director.sync());
|
||||
assertTrue(List.of(director.aliases()).contains("board"));
|
||||
assertTrue(List.of(director.aliases()).contains("sidebar"));
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisEngineSVCTest {
|
||||
@Test
|
||||
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
|
||||
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMaintenanceNeverSkipsReduction() {
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperCommandRegistrarTest {
|
||||
@Test
|
||||
public void paperCommandDelegatesExecutionAndSuggestions() {
|
||||
RecordingCommandService service = new RecordingCommandService();
|
||||
BasicCommand command = PaperCommandRegistrar.command(service);
|
||||
CommandSender sender = sender();
|
||||
CommandSourceStack source = new TestCommandSourceStack(sender);
|
||||
String[] arguments = {"create", "world"};
|
||||
|
||||
command.execute(source, arguments);
|
||||
assertSame(sender, service.sender);
|
||||
assertEquals("iris", service.label);
|
||||
assertArrayEquals(arguments, service.arguments);
|
||||
assertEquals(List.of("first", "second"), command.suggest(source, new String[]{"cr"}));
|
||||
assertTrue(command.canUse(sender));
|
||||
}
|
||||
|
||||
private static CommandSender sender() {
|
||||
return (CommandSender) Proxy.newProxyInstance(
|
||||
CommandSender.class.getClassLoader(),
|
||||
new Class<?>[]{CommandSender.class},
|
||||
(proxy, method, arguments) -> primitiveDefault(method.getReturnType())
|
||||
);
|
||||
}
|
||||
|
||||
private static Object primitiveDefault(Class<?> type) {
|
||||
if (type == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (type == byte.class) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (type == short.class) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (type == int.class) {
|
||||
return 0;
|
||||
}
|
||||
if (type == long.class) {
|
||||
return 0L;
|
||||
}
|
||||
if (type == float.class) {
|
||||
return 0F;
|
||||
}
|
||||
if (type == double.class) {
|
||||
return 0D;
|
||||
}
|
||||
if (type == char.class) {
|
||||
return '\0';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class RecordingCommandService extends CommandSVC {
|
||||
private CommandSender sender;
|
||||
private String label;
|
||||
private String[] arguments;
|
||||
|
||||
@Override
|
||||
void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
this.sender = sender;
|
||||
this.label = label;
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
return List.of("first", "second");
|
||||
}
|
||||
}
|
||||
|
||||
private record TestCommandSourceStack(CommandSender sender) implements CommandSourceStack {
|
||||
@Override
|
||||
public Location getLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getExecutor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSourceStack withLocation(Location location) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSourceStack withExecutor(Entity executor) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ClientPacketSink {
|
||||
void send(byte[] frame);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.spi.protocol.ProtocolException;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class IrisClient {
|
||||
public static final Identifier HUD_ELEMENT_ID = Identifier.fromNamespaceAndPath("irisworldgen", "pregen_hud");
|
||||
public static final Identifier KEYBIND_CATEGORY_ID = Identifier.fromNamespaceAndPath("irisworldgen", "iris");
|
||||
public static final String KEYBIND_TOGGLE_HUD = "key.irisworldgen.toggle_pregen_hud";
|
||||
public static final String KEYBIND_OPEN_MAP = "key.irisworldgen.open_vision_map";
|
||||
public static final String KEYBIND_TOGGLE_WHAT = "key.irisworldgen.toggle_what_overlay";
|
||||
|
||||
private static final IrisClientSession SESSION = new IrisClientSession();
|
||||
private static final IrisClientPregenState PREGEN = new IrisClientPregenState();
|
||||
private static final IrisClientDimension DIMENSION = new IrisClientDimension();
|
||||
private static final IrisClientTileCache TILES = new IrisClientTileCache(IrisClient::sendFrame, System::currentTimeMillis);
|
||||
private static final IrisClientCursor CURSOR = new IrisClientCursor(IrisClient::sendFrame, System::currentTimeMillis);
|
||||
private static final IrisClientMarkers MARKERS = new IrisClientMarkers();
|
||||
private static final IrisClientRegionMap REGIONS = new IrisClientRegionMap();
|
||||
private static final IrisClientToasts TOASTS = new IrisClientToasts();
|
||||
private static volatile ClientPacketSink sink;
|
||||
private static volatile boolean hudVisible = true;
|
||||
private static volatile boolean whatVisible = false;
|
||||
|
||||
private IrisClient() {
|
||||
}
|
||||
|
||||
public static IrisClientSession session() {
|
||||
return SESSION;
|
||||
}
|
||||
|
||||
public static IrisClientPregenState pregen() {
|
||||
return PREGEN;
|
||||
}
|
||||
|
||||
public static IrisClientDimension dimension() {
|
||||
return DIMENSION;
|
||||
}
|
||||
|
||||
public static IrisClientTileCache tiles() {
|
||||
return TILES;
|
||||
}
|
||||
|
||||
public static IrisClientCursor cursor() {
|
||||
return CURSOR;
|
||||
}
|
||||
|
||||
public static IrisClientMarkers markers() {
|
||||
return MARKERS;
|
||||
}
|
||||
|
||||
public static IrisClientRegionMap regions() {
|
||||
return REGIONS;
|
||||
}
|
||||
|
||||
public static IrisClientToasts toasts() {
|
||||
return TOASTS;
|
||||
}
|
||||
|
||||
public static boolean hudVisible() {
|
||||
return hudVisible;
|
||||
}
|
||||
|
||||
public static void toggleHud() {
|
||||
hudVisible = !hudVisible;
|
||||
}
|
||||
|
||||
public static boolean whatVisible() {
|
||||
return whatVisible;
|
||||
}
|
||||
|
||||
public static void toggleWhat() {
|
||||
whatVisible = !whatVisible;
|
||||
}
|
||||
|
||||
public static boolean visionAvailable() {
|
||||
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_VISION) != 0L;
|
||||
}
|
||||
|
||||
public static boolean cursorAvailable() {
|
||||
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_CURSOR) != 0L;
|
||||
}
|
||||
|
||||
public static void bindSender(ClientPacketSink boundSink) {
|
||||
sink = boundSink;
|
||||
SESSION.bind(boundSink);
|
||||
}
|
||||
|
||||
public static void sendFrame(byte[] frame) {
|
||||
ClientPacketSink activeSink = sink;
|
||||
if (activeSink != null) {
|
||||
activeSink.send(frame);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onWorldJoin() {
|
||||
SESSION.sendHello();
|
||||
}
|
||||
|
||||
public static void onDisconnect() {
|
||||
SESSION.reset();
|
||||
PREGEN.clear();
|
||||
DIMENSION.clear();
|
||||
TILES.clear();
|
||||
CURSOR.clear();
|
||||
MARKERS.clear();
|
||||
REGIONS.clear();
|
||||
TOASTS.clear();
|
||||
}
|
||||
|
||||
public static void onInbound(byte[] frame) {
|
||||
if (frame == null) {
|
||||
return;
|
||||
}
|
||||
IrisMessage message;
|
||||
try {
|
||||
message = IrisMessageCodec.decode(frame);
|
||||
} catch (ProtocolException rejected) {
|
||||
return;
|
||||
}
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
route(message);
|
||||
}
|
||||
|
||||
private static void route(IrisMessage message) {
|
||||
switch (message) {
|
||||
case IrisMessage.ServerHello serverHello -> SESSION.onServerHello(serverHello);
|
||||
case IrisMessage.PregenProgress progress -> PREGEN.onProgress(progress);
|
||||
case IrisMessage.PregenEnd end -> onPregenEnd(end.jobId());
|
||||
case IrisMessage.DimensionStatus status -> onDimensionStatus(status);
|
||||
case IrisMessage.CursorInfo cursorInfo -> CURSOR.onCursorInfo(cursorInfo);
|
||||
case IrisMessage.VisionTile visionTile -> TILES.onVisionTile(visionTile);
|
||||
case IrisMessage.VisionMarkers visionMarkers -> MARKERS.onMarkers(visionMarkers);
|
||||
case IrisMessage.PregenRegionDelta delta -> REGIONS.onDelta(delta, PREGEN.activeJobId());
|
||||
case IrisMessage.StudioHotload hotload -> TOASTS.enqueueHotload(hotload.packKey(), hotload.changedFiles(), hotload.failed(), hotload.message());
|
||||
case IrisMessage.Toast toast -> TOASTS.enqueue(toast.kind(), toast.title(), toast.body());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPregenEnd(long jobId) {
|
||||
PREGEN.onEnd(jobId);
|
||||
REGIONS.onEnd(jobId);
|
||||
}
|
||||
|
||||
private static void onDimensionStatus(IrisMessage.DimensionStatus status) {
|
||||
boolean changed = DIMENSION.onDimensionStatus(status);
|
||||
if (changed) {
|
||||
TILES.clear();
|
||||
MARKERS.clear();
|
||||
CURSOR.clear();
|
||||
REGIONS.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class IrisClientCursor {
|
||||
private static final long MIN_REQUEST_INTERVAL_MILLIS = 500L;
|
||||
|
||||
private final ClientPacketSink sink;
|
||||
private final LongSupplier clock;
|
||||
private volatile IrisMessage.CursorInfo latest;
|
||||
private int lastRequestedX;
|
||||
private int lastRequestedZ;
|
||||
private boolean requested;
|
||||
private long lastRequestMillis;
|
||||
|
||||
public IrisClientCursor(ClientPacketSink sink, LongSupplier clock) {
|
||||
this.sink = sink;
|
||||
this.clock = clock;
|
||||
this.latest = null;
|
||||
this.lastRequestedX = 0;
|
||||
this.lastRequestedZ = 0;
|
||||
this.requested = false;
|
||||
this.lastRequestMillis = 0L;
|
||||
}
|
||||
|
||||
public synchronized void requestFor(int blockX, int blockZ) {
|
||||
if (requested && blockX == lastRequestedX && blockZ == lastRequestedZ) {
|
||||
return;
|
||||
}
|
||||
long now = clock.getAsLong();
|
||||
if (now - lastRequestMillis < MIN_REQUEST_INTERVAL_MILLIS) {
|
||||
return;
|
||||
}
|
||||
sink.send(IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(blockX, blockZ)));
|
||||
lastRequestedX = blockX;
|
||||
lastRequestedZ = blockZ;
|
||||
requested = true;
|
||||
lastRequestMillis = now;
|
||||
}
|
||||
|
||||
public void onCursorInfo(IrisMessage.CursorInfo info) {
|
||||
latest = info;
|
||||
}
|
||||
|
||||
public IrisMessage.CursorInfo latest() {
|
||||
return latest;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
latest = null;
|
||||
requested = false;
|
||||
lastRequestMillis = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
public final class IrisClientDimension {
|
||||
private volatile IrisMessage.DimensionStatus status;
|
||||
|
||||
public boolean onDimensionStatus(IrisMessage.DimensionStatus incoming) {
|
||||
IrisMessage.DimensionStatus previous = status;
|
||||
status = incoming;
|
||||
if (previous == null) {
|
||||
return true;
|
||||
}
|
||||
return previous.irisWorld() != incoming.irisWorld() || !previous.dimensionKey().equals(incoming.dimensionKey());
|
||||
}
|
||||
|
||||
public IrisMessage.DimensionStatus status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean irisWorld() {
|
||||
IrisMessage.DimensionStatus current = status;
|
||||
return current != null && current.irisWorld();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
status = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
public final class IrisClientHud {
|
||||
private IrisClientHud() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
IrisToastPresenter.pump();
|
||||
IrisPregenHud.render(graphics);
|
||||
IrisWhatOverlay.render(graphics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class IrisClientKeybinds {
|
||||
private static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(IrisClient.KEYBIND_CATEGORY_ID);
|
||||
public static final KeyMapping TOGGLE_HUD = new KeyMapping(IrisClient.KEYBIND_TOGGLE_HUD, GLFW.GLFW_KEY_H, CATEGORY);
|
||||
public static final KeyMapping OPEN_MAP = new KeyMapping(IrisClient.KEYBIND_OPEN_MAP, GLFW.GLFW_KEY_M, CATEGORY);
|
||||
public static final KeyMapping TOGGLE_WHAT = new KeyMapping(IrisClient.KEYBIND_TOGGLE_WHAT, GLFW.GLFW_KEY_J, CATEGORY);
|
||||
|
||||
private IrisClientKeybinds() {
|
||||
}
|
||||
|
||||
public static KeyMapping.Category category() {
|
||||
return CATEGORY;
|
||||
}
|
||||
|
||||
public static void pollToggle() {
|
||||
while (TOGGLE_HUD.consumeClick()) {
|
||||
IrisClient.toggleHud();
|
||||
}
|
||||
while (TOGGLE_WHAT.consumeClick()) {
|
||||
IrisClient.toggleWhat();
|
||||
}
|
||||
while (OPEN_MAP.consumeClick()) {
|
||||
openVisionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private static void openVisionScreen() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.level == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
minecraft.setScreenAndShow(new IrisVisionScreen());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientMarkers {
|
||||
private final Map<IrisTileKey, List<IrisMessage.VisionMarkers.Marker>> byTile;
|
||||
|
||||
public IrisClientMarkers() {
|
||||
this.byTile = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public void onMarkers(IrisMessage.VisionMarkers markers) {
|
||||
byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers()));
|
||||
}
|
||||
|
||||
public List<IrisMessage.VisionMarkers.Marker> forTile(IrisTileKey key) {
|
||||
return byTile.get(key);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
byTile.clear();
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientPregenState {
|
||||
private final ConcurrentHashMap<Long, IrisMessage.PregenProgress> jobs;
|
||||
private volatile Long activeJobId;
|
||||
|
||||
public IrisClientPregenState() {
|
||||
this.jobs = new ConcurrentHashMap<>();
|
||||
this.activeJobId = null;
|
||||
}
|
||||
|
||||
public void onProgress(IrisMessage.PregenProgress progress) {
|
||||
jobs.put(progress.jobId(), progress);
|
||||
activeJobId = progress.jobId();
|
||||
}
|
||||
|
||||
public void onEnd(long jobId) {
|
||||
jobs.remove(jobId);
|
||||
Long current = activeJobId;
|
||||
if (current != null && current == jobId) {
|
||||
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public IrisMessage.PregenProgress active() {
|
||||
Long current = activeJobId;
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
return jobs.get(current);
|
||||
}
|
||||
|
||||
public Long activeJobId() {
|
||||
return activeJobId;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
jobs.clear();
|
||||
activeJobId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientRegionMap {
|
||||
private static final int MAX_REGIONS = 4096;
|
||||
|
||||
private final ConcurrentHashMap<Long, Integer> states;
|
||||
private volatile boolean tracking;
|
||||
private volatile long jobId;
|
||||
private volatile Bounds bounds;
|
||||
|
||||
public IrisClientRegionMap() {
|
||||
this.states = new ConcurrentHashMap<>();
|
||||
this.tracking = false;
|
||||
this.jobId = 0L;
|
||||
this.bounds = null;
|
||||
}
|
||||
|
||||
public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) {
|
||||
if (activeJobId == null) {
|
||||
return;
|
||||
}
|
||||
long active = activeJobId;
|
||||
if (delta.jobId() != active) {
|
||||
return;
|
||||
}
|
||||
if (!tracking || jobId != delta.jobId()) {
|
||||
resetTo(delta.jobId());
|
||||
}
|
||||
record(delta.regionX(), delta.regionZ(), delta.state());
|
||||
}
|
||||
|
||||
public void onEnd(long endedJobId) {
|
||||
if (tracking && jobId == endedJobId) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return tracking && !states.isEmpty();
|
||||
}
|
||||
|
||||
public Bounds bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public void forEachCell(CellConsumer consumer) {
|
||||
for (Map.Entry<Long, Integer> entry : states.entrySet()) {
|
||||
long packed = entry.getKey();
|
||||
consumer.accept(regionX(packed), regionZ(packed), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
states.clear();
|
||||
bounds = null;
|
||||
tracking = false;
|
||||
jobId = 0L;
|
||||
}
|
||||
|
||||
private void resetTo(long newJobId) {
|
||||
states.clear();
|
||||
bounds = null;
|
||||
jobId = newJobId;
|
||||
tracking = true;
|
||||
}
|
||||
|
||||
private void record(int regionX, int regionZ, int state) {
|
||||
long packed = pack(regionX, regionZ);
|
||||
if (!states.containsKey(packed)) {
|
||||
if (states.size() >= MAX_REGIONS) {
|
||||
return;
|
||||
}
|
||||
Bounds current = bounds;
|
||||
bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ);
|
||||
}
|
||||
states.put(packed, state);
|
||||
}
|
||||
|
||||
private static long pack(int regionX, int regionZ) {
|
||||
return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int regionX(long packed) {
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
private static int regionZ(long packed) {
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CellConsumer {
|
||||
void accept(int regionX, int regionZ, int state);
|
||||
}
|
||||
|
||||
public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
||||
public Bounds union(int regionX, int regionZ) {
|
||||
return new Bounds(
|
||||
Math.min(minRegionX, regionX),
|
||||
Math.min(minRegionZ, regionZ),
|
||||
Math.max(maxRegionX, regionX),
|
||||
Math.max(maxRegionZ, regionZ));
|
||||
}
|
||||
|
||||
public int regionsWide() {
|
||||
return maxRegionX - minRegionX + 1;
|
||||
}
|
||||
|
||||
public int regionsTall() {
|
||||
return maxRegionZ - minRegionZ + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
|
||||
public final class IrisClientSession {
|
||||
private static final long CLIENT_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR | IrisProtocol.CAPABILITY_STUDIO;
|
||||
|
||||
private volatile State state;
|
||||
private volatile long serverCapabilities;
|
||||
private volatile boolean irisActive;
|
||||
private volatile String serverBrand;
|
||||
private volatile ClientPacketSink sink;
|
||||
|
||||
public IrisClientSession() {
|
||||
this.state = State.IDLE;
|
||||
this.serverCapabilities = 0L;
|
||||
this.irisActive = false;
|
||||
this.serverBrand = "";
|
||||
this.sink = null;
|
||||
}
|
||||
|
||||
public void bind(ClientPacketSink boundSink) {
|
||||
this.sink = boundSink;
|
||||
}
|
||||
|
||||
public State state() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return state == State.READY;
|
||||
}
|
||||
|
||||
public boolean irisActive() {
|
||||
return irisActive;
|
||||
}
|
||||
|
||||
public long serverCapabilities() {
|
||||
return serverCapabilities;
|
||||
}
|
||||
|
||||
public String serverBrand() {
|
||||
return serverBrand;
|
||||
}
|
||||
|
||||
public void sendHello() {
|
||||
ClientPacketSink activeSink = sink;
|
||||
if (activeSink == null) {
|
||||
return;
|
||||
}
|
||||
byte[] frame = IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, CLIENT_CAPABILITIES));
|
||||
state = State.AWAITING_HELLO;
|
||||
activeSink.send(frame);
|
||||
}
|
||||
|
||||
public void onServerHello(IrisMessage.ServerHello hello) {
|
||||
this.serverCapabilities = hello.capabilities();
|
||||
this.irisActive = hello.irisActive();
|
||||
this.serverBrand = hello.serverBrand();
|
||||
this.state = State.READY;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.state = State.IDLE;
|
||||
this.serverCapabilities = 0L;
|
||||
this.irisActive = false;
|
||||
this.serverBrand = "";
|
||||
}
|
||||
|
||||
public enum State {
|
||||
IDLE,
|
||||
AWAITING_HELLO,
|
||||
READY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class IrisClientTileCache {
|
||||
private static final int MAX_CACHED_TILES = 256;
|
||||
private static final long REQUEST_RETRY_MILLIS = 3000L;
|
||||
private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND;
|
||||
|
||||
private final ClientPacketSink sink;
|
||||
private final LongSupplier clock;
|
||||
private final IrisTileAssembler assembler;
|
||||
private final LinkedHashMap<IrisTileKey, IrisTileImage> cache;
|
||||
private final Map<IrisTileKey, Long> pending;
|
||||
private final Deque<IrisTileKey> queue;
|
||||
private final Set<IrisTileKey> queued;
|
||||
private long windowStartMillis;
|
||||
private int sentInWindow;
|
||||
|
||||
public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) {
|
||||
this.sink = sink;
|
||||
this.clock = clock;
|
||||
this.assembler = new IrisTileAssembler();
|
||||
this.cache = new LinkedHashMap<>(64, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, IrisTileImage> eldest) {
|
||||
return size() > MAX_CACHED_TILES;
|
||||
}
|
||||
};
|
||||
this.pending = new HashMap<>();
|
||||
this.queue = new ArrayDeque<>();
|
||||
this.queued = new HashSet<>();
|
||||
this.windowStartMillis = 0L;
|
||||
this.sentInWindow = 0;
|
||||
}
|
||||
|
||||
public synchronized void onVisionTile(IrisMessage.VisionTile tile) {
|
||||
IrisTileImage image = assembler.add(tile);
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
|
||||
cache.put(key, image);
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
}
|
||||
|
||||
public synchronized IrisTileImage get(IrisTileKey key) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
public synchronized void resetRequestQueue() {
|
||||
queue.clear();
|
||||
queued.clear();
|
||||
}
|
||||
|
||||
public synchronized void request(IrisTileKey key) {
|
||||
if (cache.containsKey(key)) {
|
||||
return;
|
||||
}
|
||||
long now = clock.getAsLong();
|
||||
Long lastRequest = pending.get(key);
|
||||
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
|
||||
return;
|
||||
}
|
||||
if (queued.add(key)) {
|
||||
queue.addLast(key);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void pump() {
|
||||
long now = clock.getAsLong();
|
||||
if (now - windowStartMillis >= 1000L) {
|
||||
windowStartMillis = now;
|
||||
sentInWindow = 0;
|
||||
}
|
||||
while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) {
|
||||
IrisTileKey key = queue.pollFirst();
|
||||
queued.remove(key);
|
||||
if (cache.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
Long lastRequest = pending.get(key);
|
||||
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
|
||||
continue;
|
||||
}
|
||||
sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom())));
|
||||
pending.put(key, now);
|
||||
sentInWindow++;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
assembler.clear();
|
||||
cache.clear();
|
||||
pending.clear();
|
||||
queue.clear();
|
||||
queued.clear();
|
||||
sentInWindow = 0;
|
||||
windowStartMillis = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
public final class IrisClientToasts {
|
||||
private static final int MAX_PENDING = 4;
|
||||
|
||||
private final ArrayDeque<Pending> pending;
|
||||
|
||||
public IrisClientToasts() {
|
||||
this.pending = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
public void enqueue(int kind, String title, String body) {
|
||||
Pending entry = new Pending(kind, normalize(title), normalize(body));
|
||||
synchronized (pending) {
|
||||
while (pending.size() >= MAX_PENDING) {
|
||||
pending.pollFirst();
|
||||
}
|
||||
pending.addLast(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) {
|
||||
int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS;
|
||||
enqueue(kind, "Studio Hotload", hotloadBody(packKey, changedFiles, failed, message));
|
||||
}
|
||||
|
||||
public Pending poll() {
|
||||
synchronized (pending) {
|
||||
return pending.pollFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (pending) {
|
||||
pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String pack = normalize(packKey);
|
||||
if (!pack.isEmpty()) {
|
||||
builder.append(pack);
|
||||
}
|
||||
if (changedFiles != 0) {
|
||||
append(builder, changedFiles + (changedFiles == 1 ? " file" : " files"));
|
||||
}
|
||||
String text = normalize(message);
|
||||
if (!text.isEmpty()) {
|
||||
append(builder, text);
|
||||
}
|
||||
if (builder.isEmpty()) {
|
||||
return failed ? "reload failed" : "reloaded";
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static void append(StringBuilder builder, String part) {
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(" ");
|
||||
}
|
||||
builder.append(part);
|
||||
}
|
||||
|
||||
public record Pending(int kind, String title, String body) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
public final class IrisPregenHud {
|
||||
private static final int PANEL_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFD7D7D7;
|
||||
private static final int BAR_BACK_COLOR = 0xFF2B2B2B;
|
||||
private static final int BAR_RUNNING_COLOR = 0xFF66BB6A;
|
||||
private static final int PAUSED_COLOR = 0xFFFFD54F;
|
||||
private static final int GRID_BACK_COLOR = 0xFF161616;
|
||||
private static final int CELL_PENDING_COLOR = 0xFF3A3A3A;
|
||||
private static final int CELL_GENERATING_COLOR = 0xFFFFD54F;
|
||||
private static final int CELL_DONE_COLOR = 0xFF66BB6A;
|
||||
private static final int ORIGIN_X = 6;
|
||||
private static final int ORIGIN_Y = 6;
|
||||
private static final int PADDING = 4;
|
||||
private static final int ROW_GAP = 2;
|
||||
private static final int BAR_HEIGHT = 6;
|
||||
private static final int MIN_WIDTH = 130;
|
||||
private static final int MINIMAP_MAX_PX = 64;
|
||||
private static final int MINIMAP_MAX_CELL = 6;
|
||||
private static final int MINIMAP_GAP = 4;
|
||||
|
||||
private IrisPregenHud() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
if (!IrisClient.hudVisible()) {
|
||||
return;
|
||||
}
|
||||
IrisMessage.PregenProgress progress = IrisClient.pregen().active();
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
Font font = minecraft.font;
|
||||
boolean paused = progress.state() == IrisMessage.PregenProgress.STATE_PAUSED;
|
||||
double percent = progress.chunksTotal() > 0L
|
||||
? clampPercent((double) progress.chunksDone() / (double) progress.chunksTotal() * 100.0D)
|
||||
: 0.0D;
|
||||
String title = "Iris Pregen";
|
||||
String stats = String.format("%,d / %,d (%.1f%%)", progress.chunksDone(), progress.chunksTotal(), percent);
|
||||
String tail = paused ? "PAUSED" : rateAndEta(progress);
|
||||
int accent = paused ? PAUSED_COLOR : BAR_RUNNING_COLOR;
|
||||
|
||||
int lineHeight = font.lineHeight;
|
||||
int contentWidth = Math.max(MIN_WIDTH, Math.max(font.width(title), Math.max(font.width(stats), font.width(tail))));
|
||||
int contentHeight = lineHeight * 3 + ROW_GAP * 3 + BAR_HEIGHT;
|
||||
|
||||
IrisClientRegionMap regionMap = IrisClient.regions();
|
||||
IrisClientRegionMap.Bounds bounds = regionMap.hasData() ? regionMap.bounds() : null;
|
||||
boolean showMap = bounds != null;
|
||||
int cellPx = 0;
|
||||
int gridWidth = 0;
|
||||
int gridHeight = 0;
|
||||
if (showMap) {
|
||||
int span = Math.max(bounds.regionsWide(), bounds.regionsTall());
|
||||
cellPx = Math.max(1, Math.min(MINIMAP_MAX_CELL, MINIMAP_MAX_PX / span));
|
||||
gridWidth = Math.min(MINIMAP_MAX_PX, bounds.regionsWide() * cellPx);
|
||||
gridHeight = Math.min(MINIMAP_MAX_PX, bounds.regionsTall() * cellPx);
|
||||
}
|
||||
|
||||
int panelWidth = showMap ? Math.max(contentWidth, gridWidth) : contentWidth;
|
||||
int panelHeight = showMap ? contentHeight + MINIMAP_GAP + gridHeight : contentHeight;
|
||||
|
||||
graphics.fill(ORIGIN_X - PADDING, ORIGIN_Y - PADDING, ORIGIN_X + panelWidth + PADDING, ORIGIN_Y + panelHeight + PADDING, PANEL_COLOR);
|
||||
|
||||
int cursorY = ORIGIN_Y;
|
||||
graphics.text(font, title, ORIGIN_X, cursorY, TITLE_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
graphics.text(font, stats, ORIGIN_X, cursorY, TEXT_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
|
||||
int fillWidth = (int) Math.round(contentWidth * (percent / 100.0D));
|
||||
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + contentWidth, cursorY + BAR_HEIGHT, BAR_BACK_COLOR);
|
||||
if (fillWidth > 0) {
|
||||
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + fillWidth, cursorY + BAR_HEIGHT, accent);
|
||||
}
|
||||
cursorY += BAR_HEIGHT + ROW_GAP;
|
||||
|
||||
graphics.text(font, tail, ORIGIN_X, cursorY, paused ? PAUSED_COLOR : MUTED_COLOR);
|
||||
|
||||
if (showMap) {
|
||||
renderMinimap(graphics, regionMap, bounds, cellPx, gridWidth, gridHeight, ORIGIN_Y + contentHeight + MINIMAP_GAP);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderMinimap(GuiGraphicsExtractor graphics, IrisClientRegionMap regionMap, IrisClientRegionMap.Bounds bounds, int cellPx, int gridWidth, int gridHeight, int gridTop) {
|
||||
int mapLeft = ORIGIN_X;
|
||||
int mapRight = mapLeft + gridWidth;
|
||||
int mapBottom = gridTop + gridHeight;
|
||||
graphics.fill(mapLeft, gridTop, mapRight, mapBottom, GRID_BACK_COLOR);
|
||||
int minRegionX = bounds.minRegionX();
|
||||
int minRegionZ = bounds.minRegionZ();
|
||||
regionMap.forEachCell((regionX, regionZ, state) -> {
|
||||
int px = mapLeft + (regionX - minRegionX) * cellPx;
|
||||
int py = gridTop + (regionZ - minRegionZ) * cellPx;
|
||||
if (px + cellPx > mapRight || py + cellPx > mapBottom) {
|
||||
return;
|
||||
}
|
||||
graphics.fill(px, py, px + cellPx, py + cellPx, cellColor(state));
|
||||
});
|
||||
}
|
||||
|
||||
private static int cellColor(int state) {
|
||||
return switch (state) {
|
||||
case IrisMessage.PregenRegionDelta.STATE_DONE -> CELL_DONE_COLOR;
|
||||
case IrisMessage.PregenRegionDelta.STATE_GENERATING -> CELL_GENERATING_COLOR;
|
||||
default -> CELL_PENDING_COLOR;
|
||||
};
|
||||
}
|
||||
|
||||
private static String rateAndEta(IrisMessage.PregenProgress progress) {
|
||||
String rate = String.format("%,.0f/s", progress.chunksPerSecond());
|
||||
if (progress.etaMillis() > 0L) {
|
||||
return rate + " ETA " + formatDuration(progress.etaMillis());
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
private static String formatDuration(long etaMillis) {
|
||||
long totalSeconds = etaMillis / 1000L;
|
||||
long hours = totalSeconds / 3600L;
|
||||
long minutes = (totalSeconds % 3600L) / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
if (hours > 0L) {
|
||||
return hours + "h " + minutes + "m";
|
||||
}
|
||||
if (minutes > 0L) {
|
||||
return minutes + "m " + seconds + "s";
|
||||
}
|
||||
return seconds + "s";
|
||||
}
|
||||
|
||||
private static double clampPercent(double value) {
|
||||
if (value < 0.0D) {
|
||||
return 0.0D;
|
||||
}
|
||||
if (value > 100.0D) {
|
||||
return 100.0D;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class IrisTileAssembler {
|
||||
private final Map<IrisTileKey, Partial> partials;
|
||||
|
||||
IrisTileAssembler() {
|
||||
this.partials = new HashMap<>();
|
||||
}
|
||||
|
||||
IrisTileImage add(IrisMessage.VisionTile tile) {
|
||||
int chunkCount = tile.chunkCount();
|
||||
int chunkIndex = tile.chunkIndex();
|
||||
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
|
||||
return null;
|
||||
}
|
||||
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
|
||||
Partial partial = partials.get(key);
|
||||
if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) {
|
||||
if (partial != null && tile.sequence() < partial.sequence()) {
|
||||
return null;
|
||||
}
|
||||
partial = new Partial(tile.sequence(), chunkCount);
|
||||
partials.put(key, partial);
|
||||
} else if (tile.sequence() < partial.sequence()) {
|
||||
return null;
|
||||
}
|
||||
if (!partial.accept(chunkIndex, tile.data())) {
|
||||
return null;
|
||||
}
|
||||
partials.remove(key);
|
||||
return IrisTileCodec.decode(partial.concat());
|
||||
}
|
||||
|
||||
void clear() {
|
||||
partials.clear();
|
||||
}
|
||||
|
||||
private static final class Partial {
|
||||
private final int sequence;
|
||||
private final int chunkCount;
|
||||
private final byte[][] chunks;
|
||||
private int received;
|
||||
|
||||
Partial(int sequence, int chunkCount) {
|
||||
this.sequence = sequence;
|
||||
this.chunkCount = chunkCount;
|
||||
this.chunks = new byte[chunkCount][];
|
||||
this.received = 0;
|
||||
}
|
||||
|
||||
int sequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
int chunkCount() {
|
||||
return chunkCount;
|
||||
}
|
||||
|
||||
boolean accept(int chunkIndex, byte[] data) {
|
||||
if (chunks[chunkIndex] == null) {
|
||||
chunks[chunkIndex] = data;
|
||||
received++;
|
||||
}
|
||||
return received >= chunkCount;
|
||||
}
|
||||
|
||||
byte[] concat() {
|
||||
int total = 0;
|
||||
for (byte[] chunk : chunks) {
|
||||
total += chunk.length;
|
||||
}
|
||||
byte[] blob = new byte[total];
|
||||
int position = 0;
|
||||
for (byte[] chunk : chunks) {
|
||||
System.arraycopy(chunk, 0, blob, position, chunk.length);
|
||||
position += chunk.length;
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
public final class IrisTileCodec {
|
||||
public static final int MODE_RAW_RGB = 0;
|
||||
public static final int MODE_PALETTE = 1;
|
||||
private static final int MAX_DIMENSION = 512;
|
||||
private static final int OPAQUE = 0xFF000000;
|
||||
|
||||
private IrisTileCodec() {
|
||||
}
|
||||
|
||||
public static IrisTileImage decode(byte[] deflatedBlob) {
|
||||
if (deflatedBlob == null || deflatedBlob.length == 0) {
|
||||
return null;
|
||||
}
|
||||
byte[] raw = inflate(deflatedBlob);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw))) {
|
||||
int width = in.readInt();
|
||||
int height = in.readInt();
|
||||
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||||
return null;
|
||||
}
|
||||
int mode = in.readUnsignedByte();
|
||||
int[] argb = new int[width * height];
|
||||
return switch (mode) {
|
||||
case MODE_PALETTE -> decodePalette(in, width, height, argb);
|
||||
case MODE_RAW_RGB -> decodeRaw(in, width, height, argb);
|
||||
default -> null;
|
||||
};
|
||||
} catch (IOException failure) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisTileImage decodePalette(DataInputStream in, int width, int height, int[] argb) throws IOException {
|
||||
int paletteSize = in.readInt();
|
||||
if (paletteSize <= 0 || paletteSize > 256) {
|
||||
return null;
|
||||
}
|
||||
int[] palette = new int[paletteSize];
|
||||
for (int index = 0; index < paletteSize; index++) {
|
||||
int red = in.readUnsignedByte();
|
||||
int green = in.readUnsignedByte();
|
||||
int blue = in.readUnsignedByte();
|
||||
palette[index] = OPAQUE | red << 16 | green << 8 | blue;
|
||||
}
|
||||
for (int pixel = 0; pixel < argb.length; pixel++) {
|
||||
int paletteIndex = in.readUnsignedByte();
|
||||
if (paletteIndex >= paletteSize) {
|
||||
return null;
|
||||
}
|
||||
argb[pixel] = palette[paletteIndex];
|
||||
}
|
||||
return new IrisTileImage(width, height, argb);
|
||||
}
|
||||
|
||||
private static IrisTileImage decodeRaw(DataInputStream in, int width, int height, int[] argb) throws IOException {
|
||||
for (int pixel = 0; pixel < argb.length; pixel++) {
|
||||
int red = in.readUnsignedByte();
|
||||
int green = in.readUnsignedByte();
|
||||
int blue = in.readUnsignedByte();
|
||||
argb[pixel] = OPAQUE | red << 16 | green << 8 | blue;
|
||||
}
|
||||
return new IrisTileImage(width, height, argb);
|
||||
}
|
||||
|
||||
private static byte[] inflate(byte[] input) {
|
||||
Inflater inflater = new Inflater();
|
||||
inflater.setInput(input);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2));
|
||||
byte[] buffer = new byte[8192];
|
||||
try {
|
||||
while (!inflater.finished()) {
|
||||
int produced = inflater.inflate(buffer);
|
||||
if (produced == 0) {
|
||||
if (inflater.finished() || inflater.needsInput() || inflater.needsDictionary()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.write(buffer, 0, produced);
|
||||
}
|
||||
} catch (DataFormatException malformed) {
|
||||
return null;
|
||||
} finally {
|
||||
inflater.end();
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
public record IrisTileImage(int width, int height, int[] argb) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
public record IrisTileKey(int tileX, int tileZ, int zoom) {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||
import net.minecraft.client.gui.components.toasts.ToastManager;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class IrisToastPresenter {
|
||||
private IrisToastPresenter() {
|
||||
}
|
||||
|
||||
public static void pump() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.gui == null) {
|
||||
return;
|
||||
}
|
||||
ToastManager manager = minecraft.gui.toastManager();
|
||||
IrisClientToasts toasts = IrisClient.toasts();
|
||||
IrisClientToasts.Pending next = toasts.poll();
|
||||
while (next != null) {
|
||||
SystemToast.addOrUpdate(manager, tokenFor(next.kind()), Component.literal(next.title()), Component.literal(next.body()));
|
||||
next = toasts.poll();
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemToast.SystemToastId tokenFor(int kind) {
|
||||
return switch (kind) {
|
||||
case IrisMessage.Toast.KIND_SUCCESS -> SystemToast.SystemToastId.WORLD_BACKUP;
|
||||
case IrisMessage.Toast.KIND_WARNING -> SystemToast.SystemToastId.UNSECURE_SERVER_WARNING;
|
||||
case IrisMessage.Toast.KIND_ERROR -> SystemToast.SystemToastId.PACK_LOAD_FAILURE;
|
||||
default -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureManager;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class IrisVisionScreen extends Screen {
|
||||
private static final int TILE_PIXELS = 128;
|
||||
private static final int MIN_ZOOM = 0;
|
||||
private static final int MAX_ZOOM = 8;
|
||||
private static final int DEFAULT_ZOOM = 2;
|
||||
private static final int MAX_TEXTURES = 220;
|
||||
private static final int BACKGROUND_COLOR = 0xF00B0E14;
|
||||
private static final int PLACEHOLDER_COLOR = 0xFF161A22;
|
||||
private static final int GRID_COLOR = 0x33FFFFFF;
|
||||
private static final int HEADER_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFB9B9B9;
|
||||
private static final int PLAYER_FILL_COLOR = 0xFFFFDD33;
|
||||
private static final int PLAYER_BORDER_COLOR = 0xFF101010;
|
||||
private static final int MARKER_COLOR = 0xFF4FC3F7;
|
||||
private static final int MARKER_LABEL_BG = 0xE0101010;
|
||||
|
||||
private final Map<IrisTileKey, TileTexture> textures;
|
||||
private double centerBlockX;
|
||||
private double centerBlockZ;
|
||||
private int zoom;
|
||||
private boolean initialized;
|
||||
private String renderedDimensionKey;
|
||||
|
||||
public IrisVisionScreen() {
|
||||
super(Component.literal("Iris Vision"));
|
||||
this.textures = new LinkedHashMap<>(64, 0.75f, true);
|
||||
this.centerBlockX = 0.0D;
|
||||
this.centerBlockZ = 0.0D;
|
||||
this.zoom = DEFAULT_ZOOM;
|
||||
this.initialized = false;
|
||||
this.renderedDimensionKey = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (!initialized) {
|
||||
centerOnPlayer();
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
graphics.fill(0, 0, width, height, BACKGROUND_COLOR);
|
||||
IrisClientSession session = IrisClient.session();
|
||||
if (!session.isReady()) {
|
||||
drawCentered(graphics, "Connecting to Iris server...", MUTED_COLOR);
|
||||
drawHeader(graphics, "Iris Vision", "not connected");
|
||||
return;
|
||||
}
|
||||
IrisMessage.DimensionStatus status = IrisClient.dimension().status();
|
||||
if (!IrisClient.visionAvailable() || status == null) {
|
||||
drawCentered(graphics, "Not an Iris world", MUTED_COLOR);
|
||||
drawHeader(graphics, "Iris Vision", status == null ? "no dimension data" : label(status));
|
||||
return;
|
||||
}
|
||||
syncWorld(status);
|
||||
renderTiles(graphics, mouseX, mouseY, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
|
||||
int blocksPerPixel = 1 << zoom;
|
||||
centerBlockX -= dragX * blocksPerPixel;
|
||||
centerBlockZ -= dragY * blocksPerPixel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (scrollY == 0.0D) {
|
||||
return false;
|
||||
}
|
||||
int previousBlocksPerPixel = 1 << zoom;
|
||||
double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel;
|
||||
double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel;
|
||||
int updated = zoom + (scrollY > 0.0D ? -1 : 1);
|
||||
zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM));
|
||||
int newBlocksPerPixel = 1 << zoom;
|
||||
centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel;
|
||||
centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed() {
|
||||
releaseTextures();
|
||||
super.removed();
|
||||
}
|
||||
|
||||
private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) {
|
||||
int blocksPerPixel = 1 << zoom;
|
||||
int tileSpanBlocks = TILE_PIXELS * blocksPerPixel;
|
||||
int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel);
|
||||
int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel);
|
||||
int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1;
|
||||
int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1;
|
||||
int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1;
|
||||
int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1;
|
||||
int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks);
|
||||
int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks);
|
||||
|
||||
IrisClientTileCache cache = IrisClient.tiles();
|
||||
cache.resetRequestQueue();
|
||||
List<IrisTileKey> missing = new ArrayList<>();
|
||||
|
||||
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
|
||||
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
|
||||
int screenX = originX + tileX * TILE_PIXELS;
|
||||
int screenY = originY + tileZ * TILE_PIXELS;
|
||||
IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom);
|
||||
IrisTileImage image = cache.get(key);
|
||||
if (image == null) {
|
||||
graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR);
|
||||
missing.add(key);
|
||||
continue;
|
||||
}
|
||||
Identifier texture = ensureTexture(key, image);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS);
|
||||
}
|
||||
}
|
||||
|
||||
requestMissing(cache, missing, centerTileX, centerTileZ);
|
||||
cache.pump();
|
||||
evictTextures();
|
||||
|
||||
drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel);
|
||||
drawPlayer(graphics, blocksPerPixel);
|
||||
drawHeader(graphics, "Iris Vision", label(status) + " zoom " + zoom + " x" + (long) centerBlockX + " z" + (long) centerBlockZ);
|
||||
drawFooter(graphics, "Drag to pan Scroll to zoom Esc to close");
|
||||
}
|
||||
|
||||
private void requestMissing(IrisClientTileCache cache, List<IrisTileKey> missing, int centerTileX, int centerTileZ) {
|
||||
missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ)));
|
||||
for (IrisTileKey key : missing) {
|
||||
cache.request(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) {
|
||||
IrisClientMarkers markers = IrisClient.markers();
|
||||
String hoverLabel = null;
|
||||
int hoverX = 0;
|
||||
int hoverY = 0;
|
||||
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
|
||||
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
|
||||
List<IrisMessage.VisionMarkers.Marker> tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom));
|
||||
if (tileMarkers == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) {
|
||||
int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel);
|
||||
int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel);
|
||||
graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR);
|
||||
if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) {
|
||||
hoverLabel = marker.label();
|
||||
hoverX = screenX;
|
||||
hoverY = screenY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoverLabel != null) {
|
||||
int labelWidth = font.width(hoverLabel);
|
||||
graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG);
|
||||
graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) {
|
||||
LocalPlayer player = Minecraft.getInstance().player;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel);
|
||||
int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel);
|
||||
graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR);
|
||||
graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR);
|
||||
}
|
||||
|
||||
private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int headerHeight = lineHeight + 8;
|
||||
graphics.fill(0, 0, width, headerHeight, HEADER_COLOR);
|
||||
graphics.text(font, title, 8, 4, TITLE_COLOR);
|
||||
graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR);
|
||||
}
|
||||
|
||||
private void drawFooter(GuiGraphicsExtractor graphics, String hint) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int footerTop = height - lineHeight - 8;
|
||||
graphics.fill(0, footerTop, width, height, HEADER_COLOR);
|
||||
graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR);
|
||||
}
|
||||
|
||||
private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) {
|
||||
graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color);
|
||||
}
|
||||
|
||||
private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) {
|
||||
TileTexture existing = textures.get(key);
|
||||
if (existing != null) {
|
||||
return existing.id();
|
||||
}
|
||||
int tileWidth = image.width();
|
||||
int tileHeight = image.height();
|
||||
int[] argb = image.argb();
|
||||
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false);
|
||||
for (int y = 0; y < tileHeight; y++) {
|
||||
int row = y * tileWidth;
|
||||
for (int x = 0; x < tileWidth; x++) {
|
||||
nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x]));
|
||||
}
|
||||
}
|
||||
DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage);
|
||||
Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key));
|
||||
Minecraft.getInstance().getTextureManager().register(id, texture);
|
||||
textures.put(key, new TileTexture(id, texture));
|
||||
return id;
|
||||
}
|
||||
|
||||
private void evictTextures() {
|
||||
if (textures.size() <= MAX_TEXTURES) {
|
||||
return;
|
||||
}
|
||||
TextureManager manager = Minecraft.getInstance().getTextureManager();
|
||||
Iterator<Map.Entry<IrisTileKey, TileTexture>> iterator = textures.entrySet().iterator();
|
||||
while (textures.size() > MAX_TEXTURES && iterator.hasNext()) {
|
||||
Map.Entry<IrisTileKey, TileTexture> entry = iterator.next();
|
||||
manager.release(entry.getValue().id());
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseTextures() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft != null) {
|
||||
TextureManager manager = minecraft.getTextureManager();
|
||||
for (TileTexture texture : textures.values()) {
|
||||
manager.release(texture.id());
|
||||
}
|
||||
}
|
||||
textures.clear();
|
||||
}
|
||||
|
||||
private void syncWorld(IrisMessage.DimensionStatus status) {
|
||||
if (renderedDimensionKey == null) {
|
||||
renderedDimensionKey = status.dimensionKey();
|
||||
return;
|
||||
}
|
||||
if (!renderedDimensionKey.equals(status.dimensionKey())) {
|
||||
renderedDimensionKey = status.dimensionKey();
|
||||
releaseTextures();
|
||||
centerOnPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void centerOnPlayer() {
|
||||
LocalPlayer player = Minecraft.getInstance().player;
|
||||
if (player != null) {
|
||||
centerBlockX = player.getBlockX();
|
||||
centerBlockZ = player.getBlockZ();
|
||||
}
|
||||
}
|
||||
|
||||
private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) {
|
||||
long deltaX = (long) key.tileX() - centerTileX;
|
||||
long deltaZ = (long) key.tileZ() - centerTileZ;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
|
||||
private static int toAbgr(int argb) {
|
||||
int alpha = argb >>> 24 & 0xFF;
|
||||
int red = argb >> 16 & 0xFF;
|
||||
int green = argb >> 8 & 0xFF;
|
||||
int blue = argb & 0xFF;
|
||||
return alpha << 24 | blue << 16 | green << 8 | red;
|
||||
}
|
||||
|
||||
private static String texturePath(IrisTileKey key) {
|
||||
return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom();
|
||||
}
|
||||
|
||||
private static String safe(int value) {
|
||||
return value < 0 ? "m" + (-(long) value) : Integer.toString(value);
|
||||
}
|
||||
|
||||
private static String label(IrisMessage.DimensionStatus status) {
|
||||
String pack = status.packKey() == null || status.packKey().isEmpty() ? "" : " pack " + status.packKey();
|
||||
return status.dimensionKey() + pack;
|
||||
}
|
||||
|
||||
private record TileTexture(Identifier id, DynamicTexture texture) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class IrisWhatOverlay {
|
||||
private static final int PANEL_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFC7C7C7;
|
||||
private static final int PADDING = 4;
|
||||
private static final int ROW_GAP = 2;
|
||||
private static final int CURSOR_OFFSET = 12;
|
||||
|
||||
private IrisWhatOverlay() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) {
|
||||
return;
|
||||
}
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
LocalPlayer player = minecraft.player;
|
||||
int blockX = player.getBlockX();
|
||||
int blockZ = player.getBlockZ();
|
||||
HitResult hit = minecraft.hitResult;
|
||||
if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) {
|
||||
BlockPos pos = blockHit.getBlockPos();
|
||||
blockX = pos.getX();
|
||||
blockZ = pos.getZ();
|
||||
}
|
||||
IrisClient.cursor().requestFor(blockX, blockZ);
|
||||
|
||||
IrisMessage.CursorInfo info = IrisClient.cursor().latest();
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (info == null) {
|
||||
lines.add("querying " + blockX + ", " + blockZ + "...");
|
||||
} else {
|
||||
lines.add("Biome: " + display(info.biomeKey()));
|
||||
lines.add("Region: " + display(info.regionKey()));
|
||||
if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) {
|
||||
lines.add("Cave: " + display(info.caveBiomeKey()));
|
||||
}
|
||||
lines.add("Height: " + info.height() + " (" + info.blockX() + ", " + info.blockZ() + ")");
|
||||
}
|
||||
draw(graphics, minecraft.font, lines);
|
||||
}
|
||||
|
||||
private static void draw(GuiGraphicsExtractor graphics, Font font, List<String> lines) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int contentWidth = font.width("Iris What");
|
||||
for (String line : lines) {
|
||||
contentWidth = Math.max(contentWidth, font.width(line));
|
||||
}
|
||||
int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET;
|
||||
int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET;
|
||||
int rows = lines.size() + 1;
|
||||
int contentHeight = lineHeight * rows + ROW_GAP * rows;
|
||||
graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR);
|
||||
|
||||
int cursorY = originY;
|
||||
graphics.text(font, "Iris What", originX, cursorY, TITLE_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
for (String line : lines) {
|
||||
graphics.text(font, line, originX, cursorY, line.startsWith("Height") ? MUTED_COLOR : TEXT_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
private static String display(String key) {
|
||||
return key == null || key.isEmpty() ? "-" : key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.fabricmc.fabric-loom' version '1.17.14'
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
|
||||
group = 'art.arcane'
|
||||
version = irisVersion
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '../modded-common/src/main/java'
|
||||
srcDir '../minecraft-common/src/main/java'
|
||||
srcDir '../client-common/src/main/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../modded-common/src/main/resources'
|
||||
}
|
||||
}
|
||||
test {
|
||||
java {
|
||||
srcDir '../modded-common/src/test/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = 'FabricMC'
|
||||
url = uri('https://maven.fabricmc.net/')
|
||||
}
|
||||
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
|
||||
maven { url = uri('https://jitpack.io') }
|
||||
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
|
||||
}
|
||||
|
||||
configurations {
|
||||
bundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
devBundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named('bundle').configure {
|
||||
exclude(group: 'com.google.code.gson')
|
||||
exclude(group: 'com.google.guava')
|
||||
exclude(group: 'commons-io')
|
||||
exclude(group: 'org.apache.commons', module: 'commons-lang3')
|
||||
exclude(group: 'it.unimi.dsi', module: 'fastutil')
|
||||
exclude(group: 'org.slf4j')
|
||||
exclude(group: 'org.apache.logging.log4j')
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.testCompileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle)
|
||||
|
||||
configurations.create('jij') {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
List<Object> fabricApi = [
|
||||
libs.fabricApi.base,
|
||||
libs.fabricApi.registrySync,
|
||||
libs.fabricApi.resourceLoader,
|
||||
libs.fabricApi.lifecycleEvents,
|
||||
libs.fabricApi.commandApi,
|
||||
libs.fabricApi.eventsInteraction,
|
||||
libs.fabricApi.networking,
|
||||
libs.fabricApi.rendering,
|
||||
libs.fabricApi.keyMapping
|
||||
]
|
||||
fabricApi.each { Object notation ->
|
||||
add('jij', notation)
|
||||
add('implementation', notation)
|
||||
}
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
add('bundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
add('devBundle', libs.zip) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
List<Object> shared = [
|
||||
"art.arcane:core:${irisVersion}".toString(),
|
||||
"art.arcane:spi:${irisVersion}".toString(),
|
||||
libs.paralithic,
|
||||
libs.lru,
|
||||
libs.caffeine,
|
||||
libs.dom4j,
|
||||
libs.jaxen,
|
||||
libs.sentry
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
add('bundle', notation)
|
||||
add('devBundle', notation)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
|
||||
}
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
|
||||
runs {
|
||||
server {
|
||||
String parity = providers.gradleProperty('irisParity').getOrNull()
|
||||
if (parity != null) {
|
||||
property('iris.parity', parity)
|
||||
}
|
||||
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
|
||||
if (parityGolden != null) {
|
||||
property('iris.parity.golden', parityGolden)
|
||||
}
|
||||
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
|
||||
if (parityDeep != null) {
|
||||
property('iris.parity.deep', parityDeep)
|
||||
}
|
||||
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
|
||||
if (worldCheck != null) {
|
||||
property('iris.worldcheck', worldCheck)
|
||||
}
|
||||
vmArg('-Xmx8G')
|
||||
programArgs('nogui')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
inputs.property('version', project.version)
|
||||
inputs.property('minecraftVersion', minecraftVersion)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand('version': project.version, 'minecraftVersion': minecraftVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('shadowJar', ShadowJar).configure {
|
||||
doFirst {
|
||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar"))
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
mergeServiceFiles()
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
exclude('META-INF/*.SF')
|
||||
exclude('META-INF/*.DSA')
|
||||
exclude('META-INF/*.RSA')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||
exclude('oshi.properties')
|
||||
from(project.configurations.named('jij')) {
|
||||
into('META-INF/jars')
|
||||
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('assemble').configure {
|
||||
dependsOn(tasks.named('shadowJar'))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=false
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,281 @@
|
||||
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
|
||||
[05:44:33] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||
java.lang.RuntimeException: cleanup failed
|
||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[05:44:33] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||
java.lang.RuntimeException: enable failed
|
||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
Suppressed: java.lang.RuntimeException: cleanup failed
|
||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
||||
... 42 more
|
||||
[05:44:33] [Test worker/ERROR]: [worldcheck] server stop request failed
|
||||
java.lang.IllegalStateException: stop request failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[05:44:33] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
|
||||
java.lang.IllegalStateException: shutdown wait failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[05:44:33] [Test worker/ERROR]: [worldcheck] check failed
|
||||
java.lang.IllegalStateException: check failed
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
|
||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
|
||||
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
|
||||
[05:44:33] [Test worker/ERROR]: Iris custom content provider discovery failed
|
||||
java.lang.RuntimeException: provider init failed
|
||||
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.io.File
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'FabricMC'
|
||||
url = uri('https://maven.fabricmc.net/')
|
||||
}
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'iris-fabric'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
from(files('../../gradle/libs.versions.toml'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasVolmLibSettings(File directory) {
|
||||
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
|
||||
}
|
||||
|
||||
File resolveLocalVolmLibDirectory() {
|
||||
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
|
||||
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
|
||||
.orNull
|
||||
if (configuredPath != null && !configuredPath.isBlank()) {
|
||||
File configuredDirectory = file(configuredPath)
|
||||
if (hasVolmLibSettings(configuredDirectory)) {
|
||||
return configuredDirectory
|
||||
}
|
||||
}
|
||||
|
||||
File currentDirectory = settingsDir
|
||||
while (currentDirectory != null) {
|
||||
File candidate = new File(currentDirectory, 'VolmLib')
|
||||
if (hasVolmLibSettings(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
currentDirectory = currentDirectory.parentFile
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
|
||||
.orElse('true')
|
||||
.map { String value -> value.equalsIgnoreCase('true') }
|
||||
.get()
|
||||
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
|
||||
|
||||
if (useLocalVolmLib && localVolmLibDirectory != null) {
|
||||
includeBuild(localVolmLibDirectory) {
|
||||
dependencySubstitution {
|
||||
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild('../..') {
|
||||
dependencySubstitution {
|
||||
substitute(module('art.arcane:core')).using(project(':core'))
|
||||
substitute(module('art.arcane:spi')).using(project(':spi'))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.server.packs.repository.RepositorySource;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FabricForcedDatapackSources {
|
||||
private FabricForcedDatapackSources() {
|
||||
}
|
||||
|
||||
public static void attach(PackRepository repository) {
|
||||
PackRepository activeRepository = Objects.requireNonNull(repository,
|
||||
"Iris cannot attach the forced startup datapack source to a null repository");
|
||||
Set<RepositorySource> merged = new LinkedHashSet<>(activeRepository.sources);
|
||||
merged.add(ModdedForcedDatapack.repositorySource());
|
||||
activeRepository.sources = merged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedLoader;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class FabricModdedLoader implements ModdedLoader {
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "fabric";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return FabricLoader.getInstance().getModContainer("minecraft")
|
||||
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String modVersion() {
|
||||
return FabricLoader.getInstance().getModContainer("irisworldgen")
|
||||
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftServer currentServer() {
|
||||
Object instance = FabricLoader.getInstance().getGameInstance();
|
||||
return instance instanceof MinecraftServer server ? server : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clientEnvironment() {
|
||||
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path configDir() {
|
||||
return FabricLoader.getInstance().getConfigDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File modJar() {
|
||||
return FabricLoader.getInstance().getModContainer("irisworldgen")
|
||||
.flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst())
|
||||
.map((Path p) -> p.toFile())
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisPayload;
|
||||
import art.arcane.iris.modded.ModdedProtocolChannel;
|
||||
import art.arcane.iris.modded.ModdedProtocolHandler;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
public final class FabricProtocolNetworking {
|
||||
private FabricProtocolNetworking() {
|
||||
}
|
||||
|
||||
public static void install() {
|
||||
PayloadTypeRegistry.clientboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
|
||||
(payload, context) -> ModdedProtocolHandler.onInbound(context.player(), payload.data()));
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ModdedProtocolHandler.onPlayerJoin(handler.player));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> ModdedProtocolHandler.onPlayerDisconnect(handler.player));
|
||||
ModdedProtocolHandler.bindChannel(new FabricProtocolChannel());
|
||||
}
|
||||
|
||||
private static final class FabricProtocolChannel implements ModdedProtocolChannel {
|
||||
@Override
|
||||
public boolean canReceive(ServerPlayer player) {
|
||||
return ServerPlayNetworking.canSend(player, ModdedIrisPayload.TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(ServerPlayer player, ModdedIrisPayload payload) {
|
||||
ServerPlayNetworking.send(player, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user