mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-17 14:04:11 +00:00
Compare commits
294
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7e4b94bf6 | ||
|
|
a3eccd8a65 | ||
|
|
413d7c4eb2 | ||
|
|
ea9f7a79fa | ||
|
|
64e4c82716 | ||
|
|
835ad2f704 | ||
|
|
478fd4d350 | ||
|
|
97a3f7e7c3 | ||
|
|
8917bdc974 | ||
|
|
4daae925c6 | ||
|
|
0073002b57 | ||
|
|
79e7b0c79e | ||
|
|
03c2630497 | ||
|
|
b8aa00b70e | ||
|
|
62062c4a95 | ||
|
|
bf99edce87 | ||
|
|
257c7d4f87 | ||
|
|
73bf0d64a5 | ||
|
|
792f9c2da5 | ||
|
|
0b66b0b1df | ||
|
|
369c864e71 | ||
|
|
31ec5a83c2 | ||
|
|
e1049949c6 | ||
|
|
a9b394ee5d | ||
|
|
a0cf3d79bf | ||
|
|
969ec7fde3 | ||
|
|
b4df23856c | ||
|
|
e881e0a33a | ||
|
|
a7cf2cac9a | ||
|
|
62c610ce76 | ||
|
|
9a241b85cb | ||
|
|
12f30596e5 | ||
|
|
0c9ae9ed4d | ||
|
|
137724157a | ||
|
|
523cc7e7be | ||
|
|
0ca33068f3 | ||
|
|
4d79d5ed5b | ||
|
|
ad66538d7a | ||
|
|
d749633829 | ||
|
|
965e19801d | ||
|
|
d5796b275e | ||
|
|
c9786f2d23 | ||
|
|
7767d6d30a | ||
|
|
0bc235afb6 | ||
|
|
8388194129 | ||
|
|
506e545edf | ||
|
|
aedef3c38a | ||
|
|
9481a9ecf0 | ||
|
|
b8aabd4fd1 | ||
|
|
a3f607e264 | ||
|
|
191d9e88e6 | ||
|
|
3ba6d5b9c1 | ||
|
|
56ba2e51b7 | ||
|
|
038833e614 | ||
|
|
cf0587c112 | ||
|
|
a0c9d48601 | ||
|
|
4244aa4ea7 | ||
|
|
79e220298f | ||
|
|
e1393e2516 | ||
|
|
acec550625 | ||
|
|
769e077078 | ||
|
|
5633eec009 | ||
|
|
7fc8d20516 | ||
|
|
f6f381f6e4 | ||
|
|
665c6df83d | ||
|
|
26dc1d17cf | ||
|
|
8afb445b1d | ||
|
|
3246e88f88 | ||
|
|
09d36ec990 | ||
|
|
718b13df46 | ||
|
|
f17a9e0977 | ||
|
|
9dbb6b6e85 | ||
|
|
ee3af26f11 | ||
|
|
13171edafc | ||
|
|
ba2ecb7c0f | ||
|
|
57882a3f1c | ||
|
|
56178b0e0a | ||
|
|
aa3420b4fa | ||
|
|
cec9568c9a | ||
|
|
5b5f3dae89 | ||
|
|
cb7eea604e | ||
|
|
a207cdd828 | ||
|
|
0d34fae316 | ||
|
|
2889a2dbc9 | ||
|
|
baf64fb9d1 | ||
|
|
0e90d38acb | ||
|
|
acc2619bed | ||
|
|
750ee87c6b | ||
|
|
d34dd48f74 | ||
|
|
99c7559f5d | ||
|
|
65894df632 | ||
|
|
6ff13d11c3 | ||
|
|
7d8968c45a | ||
|
|
4df536a6ca | ||
|
|
54feb80295 | ||
|
|
4f32c78982 | ||
|
|
7dfcd6a989 | ||
|
|
8c0b37c0b8 | ||
|
|
4b44bcb12b | ||
|
|
2e3a41f262 | ||
|
|
45745abaa1 | ||
|
|
ac026343c1 | ||
|
|
78ddaaaf53 | ||
|
|
de90d0ec5e | ||
|
|
fac3b4e106 | ||
|
|
8757355174 | ||
|
|
2af594de61 | ||
|
|
6e77755279 | ||
|
|
12591820e8 | ||
|
|
a521109a6a | ||
|
|
a9f5145c0f | ||
|
|
e3673afb1e | ||
|
|
2145e90b1c | ||
|
|
d480abad50 | ||
|
|
be0cd567ed | ||
|
|
ce7872f667 | ||
|
|
cfa274825f | ||
|
|
e72189adc7 | ||
|
|
7f5ce70f49 | ||
|
|
a2bcd7a302 | ||
|
|
7226fc4400 | ||
|
|
c479e94c49 | ||
|
|
ee5aaec87c | ||
|
|
65ae3f1b45 | ||
|
|
a6149b27c9 | ||
|
|
4b4b418c61 | ||
|
|
8d5cf43139 | ||
|
|
5858eb366d | ||
|
|
be0d3b0156 | ||
|
|
12e8bb5a09 | ||
|
|
a765e76aa3 | ||
|
|
4ef6fc7cc6 | ||
|
|
6b82ea99e2 | ||
|
|
813f3c7bcf | ||
|
|
318c1e100b | ||
|
|
357f14e900 | ||
|
|
99cc2383d6 | ||
|
|
76d71f8344 | ||
|
|
1544c34f44 | ||
|
|
00601678d9 | ||
|
|
700dc53559 | ||
|
|
0faae93049 | ||
|
|
92310ef839 | ||
|
|
5fa6d82b79 | ||
|
|
2078bece40 | ||
|
|
077b48f47f | ||
|
|
7981fa3ce5 | ||
|
|
5c4f7bb521 | ||
|
|
0d16f8e57f | ||
|
|
cf7ebcece1 | ||
|
|
8f8a3b2355 | ||
|
|
ffc409a4ac | ||
|
|
5b5c1975c6 | ||
|
|
25c04c0157 | ||
|
|
1e25c07eac | ||
|
|
ff74a6f811 | ||
|
|
9e07009f1d | ||
|
|
53172c38ad | ||
|
|
658c6b0fde | ||
|
|
b998ff535a | ||
|
|
ba4708f2bb | ||
|
|
bda770650b | ||
|
|
8bccb48c09 | ||
|
|
a9df19cae2 | ||
|
|
4ac49baaff | ||
|
|
38a7e0fdb5 | ||
|
|
7e73e8f599 | ||
|
|
788ac06713 | ||
|
|
622b40f786 | ||
|
|
72eff9b775 | ||
|
|
789644b93f | ||
|
|
f07dfb7a85 | ||
|
|
5bffa6eb04 | ||
|
|
0b6ac19fa7 | ||
|
|
b7e6259a05 | ||
|
|
a0aa62b8c6 | ||
|
|
d80fcedd91 | ||
|
|
41664fc605 | ||
|
|
74df63f717 | ||
|
|
f46041690b | ||
|
|
b8433340c9 | ||
|
|
593c3f732a | ||
|
|
058d527c73 | ||
|
|
15826162b4 | ||
|
|
8d28ad4111 | ||
|
|
bf11d44370 | ||
|
|
f60538343e | ||
|
|
94c179fbd4 | ||
|
|
c7cca23155 | ||
|
|
b1fbede364 | ||
|
|
f55fde17d4 | ||
|
|
96a400b585 | ||
|
|
0cb3417755 | ||
|
|
8cf9e01d90 | ||
|
|
2d5af3b61e | ||
|
|
782a85c26a | ||
|
|
ea516b4a59 | ||
|
|
dd4633ad3a | ||
|
|
82fa69611e | ||
|
|
5cadd9b8c4 | ||
|
|
259f885238 | ||
|
|
54f3595ba7 | ||
|
|
a9f07ce5c6 | ||
|
|
14f99c6df3 | ||
|
|
eefcbc40ef | ||
|
|
7db46a8eb8 | ||
|
|
3a4da2086e | ||
|
|
3e4a2432bc | ||
|
|
76a07242b5 | ||
|
|
aa428c0383 | ||
|
|
c67af47dfe | ||
|
|
539cebaa21 | ||
|
|
5e95248057 | ||
|
|
6fd877ebfc | ||
|
|
4da9661885 | ||
|
|
92e5daa13e | ||
|
|
94c7181aec | ||
|
|
b7626ab23f | ||
|
|
08ffb96680 | ||
|
|
d6226240a0 | ||
|
|
85849693b6 | ||
|
|
319d249445 | ||
|
|
73af3efe12 | ||
|
|
6c7796f12f | ||
|
|
3722602a78 | ||
|
|
7b06cd51ac | ||
|
|
34684da611 | ||
|
|
c20296d106 | ||
|
|
ce1aad8a35 | ||
|
|
e4f20a7f7f | ||
|
|
bcb59d3584 | ||
|
|
7ac5ffdb81 | ||
|
|
6202ae5d06 | ||
|
|
59165df427 | ||
|
|
6bcf90bb79 | ||
|
|
ca85cd31b7 | ||
|
|
8899c9e3f9 | ||
|
|
491382abaa | ||
|
|
b4ee500c14 | ||
|
|
0695a1d707 | ||
|
|
0b2a606a25 | ||
|
|
f3b16b784b | ||
|
|
cb31510b89 | ||
|
|
7a03df5c82 | ||
|
|
adbcba3efc | ||
|
|
0ee0f62b9d | ||
|
|
55e4a70b3b | ||
|
|
53b36fe197 | ||
|
|
195f1862af | ||
|
|
c00dfbddce | ||
|
|
2fef055e5c | ||
|
|
20e10999dd | ||
|
|
2ac9609407 | ||
|
|
f4a06efa21 | ||
|
|
df299bb97d | ||
|
|
2930ca40fb | ||
|
|
0052dfec39 | ||
|
|
e31113aaa2 | ||
|
|
808e133cac | ||
|
|
8d93431319 | ||
|
|
184a04d97f | ||
|
|
08353e3bdd | ||
|
|
9d3597703d | ||
|
|
c4dcb32334 | ||
|
|
cd61b75ff6 | ||
|
|
83c6c66294 | ||
|
|
1614d6a9a7 | ||
|
|
af2167e2f7 | ||
|
|
2d5da0076c | ||
|
|
dcdda0d3c7 | ||
|
|
34f00d86cf | ||
|
|
82f52cd6ec | ||
|
|
14f3a263df | ||
|
|
d59517aeaa | ||
|
|
a35ecca7bd | ||
|
|
066017cdf6 | ||
|
|
8878e9aa5f | ||
|
|
ec1984949b | ||
|
|
f2236a8482 | ||
|
|
1df51b230d | ||
|
|
2f8d3b1d1a | ||
|
|
d0008086ac | ||
|
|
badca54e5d | ||
|
|
ec07f55fd8 | ||
|
|
b47c7ae6aa | ||
|
|
bc618e20ec | ||
|
|
2ce36eb6f1 | ||
|
|
631c486a5a | ||
|
|
8b5c109d55 | ||
|
|
3df1fba518 | ||
|
|
241260a762 | ||
|
|
c706eab60f | ||
|
|
21f8440299 | ||
|
|
8c3e5d547b |
@@ -1,85 +1,78 @@
|
||||
name: Iris Bug Report
|
||||
description: File a bug report for Iris
|
||||
labels: [ bug ]
|
||||
name: Bug report
|
||||
description: Something in Iris isn't working.
|
||||
labels: [bug]
|
||||
type: Bug
|
||||
assignees: [SwiftSwamp]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
Make sure you're on the [latest release](https://github.com/VolmitSoftware/Iris/releases/latest) and that it hasn't [already been reported](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue).
|
||||
|
||||
The Iris plugin supports Minecraft 26.1.2 through 26.3 on Paper, Purpur, Leaf, Canvas, Folia, and Spigot. The Fabric, Forge, and NeoForge builds support 26.3. Both need Java 25.
|
||||
- type: textarea
|
||||
id: how
|
||||
id: issue
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Please give a text description of how you reached the problem
|
||||
value: |
|
||||
1. Install Iris...
|
||||
2. Do this...
|
||||
3. Do that...
|
||||
4. Observe the error...
|
||||
label: What issue are you having?
|
||||
description: Include any error message you saw.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
id: steps
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Explain where you think the problem comes from (optional)
|
||||
placeholder: The code to place a is missing b and c...
|
||||
label: Steps to reproduce
|
||||
placeholder: |
|
||||
1. Start a server with Iris installed
|
||||
2. Change ... in the config
|
||||
3. Run /iris ...
|
||||
4. ... happens instead of ...
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: input
|
||||
id: diagnostics
|
||||
attributes:
|
||||
label: Debug dump or latest log file
|
||||
description: Run `/iris debug dump upload=true` and paste the link. If Iris won't start, upload `logs/latest.log` to [mclo.gs](https://mclo.gs) and paste that link instead.
|
||||
placeholder: https://mclo.gs/...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: "Iris version"
|
||||
description: Run `/iris debug version` (`/iris version` on Fabric, Forge, and NeoForge).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
|
||||
label: Server software
|
||||
options:
|
||||
- Paper
|
||||
- Purpur
|
||||
- Leaf
|
||||
- Canvas
|
||||
- Folia
|
||||
- Spigot / CraftBukkit
|
||||
- Fabric
|
||||
- Forge
|
||||
- NeoForge
|
||||
- Other (describe under Problem)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: mcversion
|
||||
attributes:
|
||||
label: Minecraft Version
|
||||
description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported.
|
||||
options:
|
||||
- '26.2'
|
||||
- "Paper"
|
||||
- "Purpur"
|
||||
- "Leaf"
|
||||
- "Canvas"
|
||||
- "Folia"
|
||||
- "Spigot"
|
||||
- "Fabric"
|
||||
- "Forge"
|
||||
- "NeoForge"
|
||||
- Other (unsupported)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: loaderversion
|
||||
- type: dropdown
|
||||
id: minecraft
|
||||
attributes:
|
||||
label: Platform / Loader Version
|
||||
description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta".
|
||||
placeholder: DO NOT SAY "LATEST"
|
||||
label: Minecraft version
|
||||
options:
|
||||
- "26.3"
|
||||
- "26.2"
|
||||
- "26.1.2"
|
||||
- Other (unsupported)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: irisversion
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Iris Version
|
||||
description: What version of Iris are you running? (see console)
|
||||
placeholder: DO NOT SAY "LATEST"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log
|
||||
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
|
||||
placeholder: https://mslog.gs/...
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
label: Anything else?
|
||||
description: Other plugins involved, screenshots, or videos.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Help and questions
|
||||
url: https://volmitsoftware.com/discord
|
||||
about: Ask setup and configuration questions on the Volmit Discord.
|
||||
- name: "Iris documentation"
|
||||
url: https://docs.volmitsoftware.com/iris
|
||||
about: Installation, commands, permissions, and configuration.
|
||||
@@ -1,46 +1,28 @@
|
||||
name: Iris Feature Request
|
||||
description: File a feature request for Iris. If you want to report a bug this is not the place.
|
||||
labels: [ feature ]
|
||||
name: Suggestion
|
||||
description: Suggest a new feature or a change to Iris.
|
||||
labels: [enhancement]
|
||||
type: Feature
|
||||
assignees: [SwiftSwamp]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
- type: dropdown
|
||||
id: arc
|
||||
Check the [existing suggestions](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue+label%3Aenhancement) first and add relevant information to the existing issue.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Adding, Removing, or Changing
|
||||
description: What are you doing
|
||||
options:
|
||||
- Adding
|
||||
- Removing
|
||||
- Changing
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: atype
|
||||
attributes:
|
||||
label: Type of Modification
|
||||
description: What is it for?
|
||||
value: I want to ...
|
||||
label: What are your suggestions for new features or changes?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: desc
|
||||
id: proposal
|
||||
attributes:
|
||||
label: What are you trying to modify
|
||||
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
|
||||
value: The way I would implement this is ...
|
||||
label: How should it work?
|
||||
description: Provide as much detail as possible on what you would like changed or added.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
id: extra
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: What alternatives have you considered?
|
||||
value: If this could not be implemented I would ...
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
label: Anything else?
|
||||
description: Any additional information such as screenshots, examples, etc.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Translation
|
||||
description: A message in Iris is wrong, missing, or unclear.
|
||||
labels: [bug]
|
||||
type: Bug
|
||||
assignees: [SwiftSwamp]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
The language files are in [`core/src/main/resources/languages/`](https://github.com/VolmitSoftware/Iris/tree/HEAD/core/src/main/resources/languages).
|
||||
|
||||
You can create a pull request to modify the files directly.
|
||||
- type: dropdown
|
||||
id: language
|
||||
attributes:
|
||||
label: Language
|
||||
options:
|
||||
- English (en_US)
|
||||
- German (de_DE)
|
||||
- Spanish (es_ES)
|
||||
- Finnish (fi_FI)
|
||||
- French (fr_FR)
|
||||
- Hebrew (he_IL)
|
||||
- Italian (it_IT)
|
||||
- Japanese (ja-JP)
|
||||
- Korean (ko_KR)
|
||||
- Lithuanian (lt_LT)
|
||||
- Dutch (nl_NL)
|
||||
- Polish (pl_PL)
|
||||
- Portuguese (pt_PT)
|
||||
- Russian (ru_RU)
|
||||
- Turkish (tr_TR)
|
||||
- Vietnamese (vi_VI)
|
||||
- Chinese, Simplified (zh_CN)
|
||||
- Chinese, Traditional (zh_TW)
|
||||
- Another language (name it below)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current
|
||||
attributes:
|
||||
label: Current text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested
|
||||
attributes:
|
||||
label: Suggested text
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: where
|
||||
attributes:
|
||||
label: Where does it appear?
|
||||
description: The command used or message key where it is appearing.
|
||||
@@ -76,3 +76,8 @@ BUILDS/
|
||||
.compatwt/
|
||||
|
||||
.worktrees/
|
||||
|
||||
/VolmLib/
|
||||
/docs/
|
||||
/.perf/
|
||||
/.codegraph/
|
||||
|
||||
+452
-296
@@ -1,160 +1,215 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the
|
||||
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
|
||||
versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use
|
||||
the GNU General Public License for most of our software; it applies also to any other work released this way by its
|
||||
authors. You can apply it to your programs, too.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make
|
||||
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
|
||||
source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and
|
||||
that you know you can do these things.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights.
|
||||
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
|
||||
responsibilities to respect the freedom of others.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients
|
||||
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
|
||||
show them these terms so they know their rights.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute
|
||||
and/or modify it.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
|
||||
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
|
||||
will not be attributed erroneously to authors of previous versions.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run modified versions of the software inside them, although
|
||||
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
|
||||
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
|
||||
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
|
||||
domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict
|
||||
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
|
||||
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "
|
||||
Licensees" and
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission,
|
||||
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
|
||||
work "based on" the earlier work.
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based on the Program.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily
|
||||
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
|
||||
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction
|
||||
with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright
|
||||
notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the extent that warranties are provided), that
|
||||
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
|
||||
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means
|
||||
any non-source form of a work.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body,
|
||||
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
|
||||
working in that language.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in
|
||||
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
|
||||
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
|
||||
available to the public in source code form. A
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used
|
||||
to run it.
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (
|
||||
for an executable work) run the object code and to modify the work, including scripts to control those activities.
|
||||
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
|
||||
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
|
||||
Source includes interface definition files associated with source files for the work, and the source code for shared
|
||||
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
|
||||
communication or control flow between those subprograms and other parts of the work.
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the
|
||||
Corresponding Source.
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same work.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided
|
||||
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
|
||||
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
|
||||
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license
|
||||
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
|
||||
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
|
||||
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
|
||||
prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not
|
||||
allowed; section 10 makes it unnecessary.
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling
|
||||
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
|
||||
restricting circumvention of such measures.
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the
|
||||
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
|
||||
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
|
||||
your or third parties' legal rights to forbid circumvention of technological measures.
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
|
||||
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
|
||||
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for
|
||||
a fee.
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source
|
||||
code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
@@ -177,17 +232,22 @@ code under the terms of section 4, provided that you also meet all of these cond
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of
|
||||
the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or
|
||||
distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
|
||||
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
|
||||
this License to apply to the other parts of the aggregate.
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
@@ -230,55 +290,77 @@ machine-readable Corresponding Source under the terms of this License, in one of
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
|
||||
need not be included in conveying the object code work.
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used
|
||||
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
|
||||
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
|
||||
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
|
||||
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
|
||||
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
|
||||
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
|
||||
the product.
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information
|
||||
required to install and execute modified versions of a covered work in that User Product from a modified version of its
|
||||
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
|
||||
is in no case prevented or interfered with solely because modification has been made.
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the
|
||||
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
|
||||
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
|
||||
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
|
||||
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
|
||||
example, the work has been installed in ROM).
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a requirement to continue to provide support
|
||||
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
|
||||
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
|
||||
and adversely affects the operation of the network or violates the rules and protocols for communication across the
|
||||
network.
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format
|
||||
that is publicly documented (and with an implementation available to the public in source code form), and must require
|
||||
no special password or key for unpacking, reading or copying.
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of
|
||||
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
|
||||
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
|
||||
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
|
||||
governed by this License without regard to the additional permissions.
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or
|
||||
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
|
||||
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
|
||||
or can give appropriate copyright permission.
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by
|
||||
the copyright holders of that material) supplement the terms of this License with terms:
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
@@ -303,182 +385,251 @@ the copyright holders of that material) supplement the terms of this License wit
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
|
||||
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
|
||||
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
|
||||
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
|
||||
that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a
|
||||
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as
|
||||
exceptions; the above requirements apply either way.
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to
|
||||
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
|
||||
licenses granted under the third paragraph of section 11).
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license from a particular copyright holder is
|
||||
reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
|
||||
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
|
||||
after the cessation.
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you
|
||||
of the violation by some reasonable means, this is the first time you have received notice of violation of this
|
||||
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
|
||||
notice.
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or
|
||||
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
|
||||
qualify to receive new licenses for the same material under section 10.
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a
|
||||
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
|
||||
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
|
||||
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run,
|
||||
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
|
||||
parties with this License.
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or
|
||||
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
|
||||
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
|
||||
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
|
||||
reasonable efforts.
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For
|
||||
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
|
||||
and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
|
||||
selling, offering for sale, or importing the Program or any portion of it.
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the
|
||||
Program is based. The work thus licensed is called the contributor's "contributor version".
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already
|
||||
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
|
||||
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
|
||||
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
|
||||
sublicenses in a manner consistent with the requirements of this License.
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential
|
||||
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
|
||||
contributor version.
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not
|
||||
to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a
|
||||
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not
|
||||
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
|
||||
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
|
||||
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
|
||||
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
|
||||
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
|
||||
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
|
||||
that country that you have reason to believe are valid.
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring
|
||||
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
|
||||
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
|
||||
automatically extended to all recipients of the covered work and works based on it.
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of,
|
||||
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
|
||||
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
|
||||
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
|
||||
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
|
||||
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
|
||||
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to
|
||||
infringement that may otherwise be available to you under applicable patent law.
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this
|
||||
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
|
||||
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
|
||||
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
|
||||
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
|
||||
would be to refrain entirely from conveying the Program.
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work
|
||||
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
|
||||
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
|
||||
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
|
||||
to the combination as such.
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to
|
||||
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
|
||||
problems or concerns.
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the
|
||||
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
|
||||
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
|
||||
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used,
|
||||
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
|
||||
Program.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed
|
||||
on any author or copyright holder as a result of your choosing to follow a later version.
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING
|
||||
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
|
||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
||||
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
|
||||
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
||||
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to
|
||||
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
|
||||
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
|
||||
in return for a fee.
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve
|
||||
this is to make it free software which everyone can redistribute and change under these terms.
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to
|
||||
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
|
||||
to where the full notice is found.
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
@@ -498,21 +649,26 @@ to where the full notice is found.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of
|
||||
course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for
|
||||
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
|
||||
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
|
||||
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
@@ -4,7 +4,7 @@ Iris is a world generation engine for Minecraft servers and mod loaders. It gene
|
||||
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. Cross-platform generation targets deterministic parity for identical
|
||||
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2.
|
||||
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.3.
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
@@ -20,12 +20,12 @@ Canonical English is defined in the typed Java catalogs under `core/src/main/jav
|
||||
|
||||
| Platform | Artifact | Minecraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set |
|
||||
| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 |
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.3 | Full feature set |
|
||||
| Folia | plugin jar | 26.1.2 - 26.3 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.3 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
|
||||
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
|
||||
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
|
||||
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; requires NeoForge 26.3.0.65-beta+; current target is NeoForge 26.3.0.83 |
|
||||
|
||||
Java 25 is required on every platform.
|
||||
|
||||
@@ -252,7 +252,9 @@ Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everyth
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
|
||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory: the Bukkit jar
|
||||
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
|
||||
(1.21.1, 26.1.2, 26.2 and 26.3):
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
@@ -261,16 +263,26 @@ Iris v<version> [Forge] <mc>+<loader>.jar
|
||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
||||
```
|
||||
|
||||
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
|
||||
Per-platform tasks: `./gradlew buildBukkit`, and `build<Loader><mc>` for one mod jar, for example
|
||||
`buildFabric26_2` or `buildNeoForge1_21_1`. `buildFabric`, `buildForge` and `buildNeoforge` export the
|
||||
26.3 jars. `./gradlew checkAll` runs every check, then builds and verifies all twelve mod jars and runs their
|
||||
unit tests; Forge 1.21.1 only compiles its tests because Forge 52 cannot bootstrap Minecraft outside
|
||||
FML. The
|
||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
||||
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
|
||||
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
|
||||
changes only the consumer directory. Both tasks copy all four verified platform jars into
|
||||
changes only the consumer directory. Both tasks copy every verified platform jar into
|
||||
`../PluginOuts/`.
|
||||
|
||||
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
|
||||
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
|
||||
`-PuseLocalVolmLib=false`, the build uses VolmLib `master-SNAPSHOT` from JitPack. There is no VolmLib
|
||||
version to bump. The build logs which VolmLib it used, and every jar records it in
|
||||
`META-INF/volmit/volmlib.properties`.
|
||||
|
||||
If you need help compiling as a developer or contributor, ask in the Discord.
|
||||
|
||||
## Adapters / modded development
|
||||
@@ -298,9 +310,10 @@ properties:
|
||||
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
|
||||
|
||||
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
|
||||
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
|
||||
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
|
||||
screens); every adapter adds those source directories, so one edit reaches all three loaders.
|
||||
Shared code lives in `adapters/modded-common` (loaders + the shared test suite) and
|
||||
`adapters/client-common` (client HUD and world-type screens); every adapter adds those source
|
||||
directories, so one edit reaches all three loaders. Shared Minecraft-native code lives in VolmLib
|
||||
(`native/minecraft-common`).
|
||||
|
||||
For IDE import you can surface the three adapter builds in the root composite with
|
||||
`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to
|
||||
|
||||
+36
-31
@@ -4,12 +4,12 @@ import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeChunkGenerator;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainColumns;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -24,7 +24,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
@@ -44,68 +45,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
}
|
||||
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
|
||||
verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
|
||||
|
||||
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
when(fixture.complex().resolvedTerrainColumn(0, 0))
|
||||
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
|
||||
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
|
||||
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
|
||||
|
||||
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
||||
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
|
||||
verify(fixture.engine(), times(1)).getHeight(0, 0, true);
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
|
||||
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
|
||||
when(policy.openQuery(any())).thenReturn(session);
|
||||
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
|
||||
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
LevelHeightAccessor.create(-16, 2), null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
LevelHeightAccessor.create(-14, 3), null));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
verify(session, times(6)).resolvedColumn();
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
|
||||
return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
|
||||
}
|
||||
|
||||
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
|
||||
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
||||
return new Fixture(generator, engine, complex);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
|
||||
@@ -127,6 +132,6 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
|
||||
}
|
||||
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) {
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ChunkLevel;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkRetentionTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
|
||||
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
|
||||
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
|
||||
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
|
||||
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
|
||||
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
|
||||
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
|
||||
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
ChunkHolderManager manager = mock(ChunkHolderManager.class);
|
||||
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
|
||||
managerField.setAccessible(true);
|
||||
managerField.set(scheduler, manager);
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
|
||||
.thenReturn(true);
|
||||
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
|
||||
.thenReturn(true);
|
||||
|
||||
assertTrue(binding.retainChunk(world, 3, -4));
|
||||
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
|
||||
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
|
||||
assertTrue(type.getValue().doesLoad());
|
||||
assertFalse(type.getValue().doesSimulate());
|
||||
assertFalse(type.getValue().persist());
|
||||
|
||||
assertTrue(binding.releaseChunk(world, 3, -4));
|
||||
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
|
||||
binding.retainChunk(world, 3, -4);
|
||||
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
|
||||
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
|
||||
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
assertSame(type.getValue(), again.getValue());
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -15,7 +15,6 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeGenerationRegistryImpl;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
@@ -49,7 +48,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
||||
return access;
|
||||
}
|
||||
throw new IllegalStateException("Generation registry contract requires loaded native registries");
|
||||
}), "bukkit-generation-registry-v1", new NativeWorldGenerationImpl().generationRendererIdentity());
|
||||
}), "bukkit-generation-registry-v1");
|
||||
nativeAccess = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(registries,
|
||||
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
|
||||
message -> { throw new IllegalStateException("Unavailable native registry: " + message); }));
|
||||
|
||||
+42
-31
@@ -4,16 +4,17 @@ import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeChunkGenerator;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -24,7 +25,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -32,6 +33,7 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
@@ -44,69 +46,78 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
NativeBlockTags.bindHeightmapFixtures();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void restoreBlockTags() {
|
||||
NativeBlockTags.restoreTags();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
}
|
||||
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
|
||||
verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
|
||||
|
||||
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
when(fixture.complex().resolvedTerrainColumn(0, 0))
|
||||
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
|
||||
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
|
||||
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
|
||||
|
||||
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
||||
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
|
||||
verify(fixture.engine(), times(1)).getHeight(0, 0, true);
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
|
||||
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
|
||||
when(policy.openQuery(any())).thenReturn(session);
|
||||
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
|
||||
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
LevelHeightAccessor.create(-16, 2), null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
LevelHeightAccessor.create(-14, 3), null));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
verify(session, times(6)).resolvedColumn();
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
|
||||
return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
|
||||
}
|
||||
|
||||
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
|
||||
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
||||
return new Fixture(generator, engine, complex);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
|
||||
@@ -128,6 +139,6 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
|
||||
}
|
||||
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) {
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest {
|
||||
NativeBlockTags.bindHeightmapFixtures();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void restoreBlockTags() {
|
||||
NativeBlockTags.restoreTags();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
|
||||
+17
@@ -1,21 +1,30 @@
|
||||
package art.arcane.iris.nativegen.v26_3_R1;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.TagLoader;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class NativeBlockTags {
|
||||
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
|
||||
|
||||
private NativeBlockTags() {
|
||||
}
|
||||
|
||||
public static void bindHeightmapFixtures() {
|
||||
originalTags = new HashMap<>();
|
||||
for (HolderSet.Named<Block> tag : BuiltInRegistries.BLOCK.getTags().toList()) {
|
||||
originalTags.put(tag.key(), tag.stream().toList());
|
||||
}
|
||||
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
||||
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||
@@ -25,4 +34,12 @@ public final class NativeBlockTags {
|
||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
||||
}
|
||||
|
||||
public static void restoreTags() {
|
||||
if (originalTags == null) {
|
||||
return;
|
||||
}
|
||||
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, originalTags)).apply();
|
||||
originalTags = null;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -13,6 +13,7 @@ import net.minecraft.data.registries.VanillaRegistries;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.attribute.modifier.MobSpawnSettingsModifier;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
@@ -53,5 +54,7 @@ public class CustomBiomeCodecTest {
|
||||
assertEquals(2, decoded.count().minInclusive());
|
||||
assertEquals(4, decoded.count().maxInclusive());
|
||||
assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF);
|
||||
assertEquals(MobSpawnSettingsModifier.overlay(),
|
||||
biome.getAttributes().get(EnvironmentAttributes.NATURAL_MOB_SPAWNS).modifier());
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ChunkLevel;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkRetentionTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
|
||||
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
|
||||
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
|
||||
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
|
||||
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
|
||||
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
|
||||
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
|
||||
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
ChunkHolderManager manager = mock(ChunkHolderManager.class);
|
||||
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
|
||||
managerField.setAccessible(true);
|
||||
managerField.set(scheduler, manager);
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
|
||||
.thenReturn(true);
|
||||
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
|
||||
.thenReturn(true);
|
||||
|
||||
assertTrue(binding.retainChunk(world, 3, -4));
|
||||
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
|
||||
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
|
||||
assertTrue(type.getValue().doesLoad());
|
||||
assertFalse(type.getValue().doesSimulate());
|
||||
assertFalse(type.getValue().persist());
|
||||
|
||||
assertTrue(binding.releaseChunk(world, 3, -4));
|
||||
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
|
||||
binding.retainChunk(world, 3, -4);
|
||||
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
|
||||
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
|
||||
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
|
||||
assertSame(type.getValue(), again.getValue());
|
||||
}
|
||||
}
|
||||
+6
@@ -34,6 +34,7 @@ import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest {
|
||||
NativeBlockTags.bindHeightmapFixtures();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void restoreBlockTags() {
|
||||
NativeBlockTags.restoreTags();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
|
||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class BareMobSpawnHistoryTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
|
||||
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
|
||||
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
|
||||
new GenerationRegistryContract.PhysicalResourceKey(
|
||||
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||
"iris:biomes/" + "c".repeat(64)
|
||||
);
|
||||
private static HeadlessNativeRegistries loaded;
|
||||
private static PlatformGenerationRegistry registry;
|
||||
|
||||
@BeforeClass
|
||||
public static void installOverlayRendering() throws IOException {
|
||||
assertNull(Bukkit.getServer());
|
||||
loaded = HeadlessNativeRegistries.load(Map.of(
|
||||
"data/iris/worldgen/biome/biomes/" + "c".repeat(64) + ".json",
|
||||
spawningBiome().generateJson(new DataFixerV263())
|
||||
));
|
||||
registry = new HeadlessNativePlatform(new File("unused-native-platform"), loaded::registries)
|
||||
.generationRegistry();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeRegistries() throws IOException {
|
||||
if (loaded != null) {
|
||||
loaded.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bareMobSpawnHistoryValidatesAgainstTheInstalledOverlayRendering() throws IOException {
|
||||
GenerationRegistryContract legacy = legacyContract(INSTALLED, spawningBiome());
|
||||
GenerationRegistryContract.GeneratedSource source = legacy.generatedSources().get(INSTALLED);
|
||||
assertEquals(GenerationRegistryContract.FINGERPRINT_SCHEMA_VERSION_ONE, legacy.fingerprintSchema());
|
||||
assertEquals(LEGACY_RENDERER_IDENTITY, source.rendererIdentity());
|
||||
assertNotEquals(
|
||||
JsonParser.parseString(source.sourceJson()),
|
||||
JsonParser.parseString(registry.generatedDefinition(
|
||||
INSTALLED.registryKey(),
|
||||
INSTALLED.resourceKey()
|
||||
).value())
|
||||
);
|
||||
|
||||
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
|
||||
Set.of(legacy),
|
||||
new DataFixerV263(),
|
||||
registry
|
||||
);
|
||||
|
||||
legacy.requireDefinitionsAvailableIn(available);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bareMobSpawnHistoryStillRejectsDifferentSemantics() throws IOException {
|
||||
GenerationRegistryContract changed = legacyContract(INSTALLED, spawningBiome().setTemperature(0.2D));
|
||||
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
|
||||
Set.of(changed),
|
||||
new DataFixerV263(),
|
||||
registry
|
||||
));
|
||||
|
||||
assertEquals("Historical generated registry definition changed for minecraft:worldgen/biome / "
|
||||
+ INSTALLED.resourceKey() + ".", failure.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bareMobSpawnHistoryStillRejectsAMissingDefinition() throws IOException {
|
||||
GenerationRegistryContract missing = legacyContract(new GenerationRegistryContract.PhysicalResourceKey(
|
||||
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||
"iris:biomes/" + "d".repeat(64)
|
||||
), spawningBiome());
|
||||
|
||||
assertThrows(IOException.class, () -> GenerationRegistryContractFactory.captureRequiredDefinitions(
|
||||
Set.of(missing),
|
||||
new DataFixerV263(),
|
||||
registry
|
||||
));
|
||||
}
|
||||
|
||||
private static GenerationRegistryContract legacyContract(
|
||||
GenerationRegistryContract.PhysicalResourceKey key,
|
||||
IrisBiomeCustom biome
|
||||
) {
|
||||
JsonObject rendered = JsonParser.parseString(biome.generateJson(new DataFixerV263())).getAsJsonObject();
|
||||
JsonObject attributes = rendered.getAsJsonObject("attributes");
|
||||
attributes.add(NATURAL_MOB_SPAWNS, attributes.getAsJsonObject(NATURAL_MOB_SPAWNS).get("argument"));
|
||||
String captured = registry.canonicalDefinition(key.registryKey(), key.resourceKey(), rendered.toString())
|
||||
.value();
|
||||
String semantic = GenerationRegistryContractFactory.customBiomeEffectiveSemanticJson(biome, null);
|
||||
GenerationRegistryContract.GeneratedSource source = new GenerationRegistryContract.GeneratedSource(
|
||||
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
|
||||
GenerationRegistryContractFactory.fingerprintCustomBiomeAuthoredDefinition(semantic),
|
||||
semantic,
|
||||
LEGACY_RENDERER_IDENTITY,
|
||||
GenerationRegistryContractFactory.fingerprintDefinition(
|
||||
key,
|
||||
PlatformGenerationRegistry.Definition.exactJson(captured),
|
||||
"generated"
|
||||
),
|
||||
captured
|
||||
);
|
||||
return GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
|
||||
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, source)),
|
||||
Map.of(key, source)
|
||||
);
|
||||
}
|
||||
|
||||
private static IrisBiomeCustom spawningBiome() {
|
||||
return new IrisBiomeCustom()
|
||||
.setId("tester")
|
||||
.setSpawnRarity(2)
|
||||
.setSpawns(new KList<>(
|
||||
new IrisBiomeCustomSpawn()
|
||||
.setType("minecraft:zombie")
|
||||
.setGroup(IrisBiomeCustomSpawnType.MONSTER)
|
||||
.setMinCount(2)
|
||||
.setMaxCount(4)
|
||||
.setWeight(7),
|
||||
new IrisBiomeCustomSpawn()
|
||||
.setType("minecraft:cow")
|
||||
.setGroup(IrisBiomeCustomSpawnType.CREATURE)
|
||||
.setMinCount(3)
|
||||
.setMaxCount(3)
|
||||
.setWeight(5)
|
||||
));
|
||||
}
|
||||
}
|
||||
+4
@@ -29,6 +29,7 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
@@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class HeadlessChunkGeneratorTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
|
||||
+4
@@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
@@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessChunkLightingTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
|
||||
+4
@@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
@@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HeadlessChunkSerializationTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.Test;
|
||||
@@ -22,8 +21,6 @@ public final class HeadlessGenerationRegistryTest {
|
||||
HeadlessNativeTestRegistries::get);
|
||||
PlatformGenerationRegistry registry = platform.generationRegistry();
|
||||
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
|
||||
assertEquals(new NativeWorldGenerationImpl().generationRendererIdentity(),
|
||||
registry.generatedDefinitionRendererIdentity());
|
||||
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
|
||||
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
|
||||
assertEquals(registry.canonicalDefinition("minecraft:dimension_type", "iris:native_test", dimension),
|
||||
|
||||
+4
@@ -9,6 +9,7 @@ import net.minecraft.world.item.Items;
|
||||
|
||||
import java.util.Map;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
@@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessNativeBootstrapTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Test
|
||||
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
||||
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
||||
|
||||
+4
@@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessNativeRegistriesTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Test
|
||||
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
||||
|
||||
+4
@@ -4,6 +4,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HeadlessNativeRegistryTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HeadlessNativeTileTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
|
||||
+4
@@ -17,6 +17,7 @@ import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
@@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessRegionTerrainTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
|
||||
+4
@@ -24,6 +24,7 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
@@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NativeRegionTerrainWriterTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
|
||||
+4
@@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class OfflineNativeFeatureFeasibilityTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
private static HeadlessNativeRegistries loaded;
|
||||
private static RegistryAccess registries;
|
||||
private static DimensionType dimension;
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
@@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public final class OfflineRegionPublicationTest {
|
||||
@ClassRule
|
||||
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PaperRegistryIsolation implements TestRule {
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
Map<Object, Object> registries = registries();
|
||||
Map<Object, Object> previous = new HashMap<>(registries);
|
||||
registries.clear();
|
||||
try {
|
||||
base.evaluate();
|
||||
} finally {
|
||||
registries.clear();
|
||||
registries.putAll(previous);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<Object, Object> registries() throws ReflectiveOperationException {
|
||||
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
||||
field.setAccessible(true);
|
||||
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,11 @@ String apiVersion = providers.gradleProperty('apiVersion').get()
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
compileOnly(volmLibCoordinate) {
|
||||
compileOnly('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.paper.api)
|
||||
@@ -23,7 +19,7 @@ dependencies {
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(testFixtures(project(':core')))
|
||||
testImplementation(volmLibCoordinate) {
|
||||
testImplementation('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import art.arcane.iris.world.IrisStartupValidation;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Keeps a failed enable from handing Iris worlds to the vanilla generator.
|
||||
* <p>
|
||||
* Paper disables a plugin whose onEnable fails, and CraftServer builds every world bukkit.yml points at a disabled
|
||||
* generator plugin with the vanilla generator. On a server with Iris worlds, Iris therefore stays enabled with its
|
||||
* runtime locked: the generator resolver answers every Iris world with a fail-closed generator, which stops the
|
||||
* level before it exists, and the admission listener refuses logins. It only needs IrisStartupValidation and
|
||||
* logging, never a service the failed enable may not have created.
|
||||
*/
|
||||
final class FailedEnableLock {
|
||||
private FailedEnableLock() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when Iris must stay enabled and locked, false when the failure may disable Iris as before
|
||||
*/
|
||||
static boolean engage(Throwable failure, boolean irisWorldsPresent, boolean startupWorldsCreated) {
|
||||
if (!irisWorldsPresent) {
|
||||
return false;
|
||||
}
|
||||
IrisStartupValidation.markRuntimeInvalid("Iris failed to enable: " + GenerationRefusalNotice.summary(failure));
|
||||
List<String> notice = notice(failure, startupWorldsCreated);
|
||||
try {
|
||||
for (String line : notice) {
|
||||
Iris.error(line);
|
||||
}
|
||||
Iris.reportError(failure);
|
||||
} catch (Throwable loggingFailure) {
|
||||
notice.forEach(System.err::println);
|
||||
failure.addSuppressed(loggingFailure);
|
||||
failure.printStackTrace(System.err);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deferring teardown to the server stop quiesces through the NMS binding and waits for generators Paper closes.
|
||||
* A failed enable may have neither, so it tears down at once even while the server stops.
|
||||
*/
|
||||
static boolean defersTeardownToServerStop(boolean enableFailed, boolean serverStopping) {
|
||||
return !enableFailed && serverStopping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startupWorldsCreated true when Iris enabled on a running server (a hotload), whose startup worlds
|
||||
* already exist, so nothing stops
|
||||
*/
|
||||
static List<String> notice(Throwable failure, boolean startupWorldsCreated) {
|
||||
return GenerationRefusalNotice.compose(
|
||||
"Iris failed to enable",
|
||||
GenerationRefusalNotice.causes(failure),
|
||||
List.of(
|
||||
"Iris stays enabled with a locked runtime so no Iris world falls back to vanilla generation.",
|
||||
startupWorldsCreated
|
||||
? "Every Iris world that is not loaded yet refuses to load; the server keeps running"
|
||||
+ " and no chunks are written for them."
|
||||
: "Every Iris world refuses to generate: server startup stops before the first one"
|
||||
+ " loads, and no chunks are written.",
|
||||
"Player logins are refused until this is fixed and the server restarts."
|
||||
));
|
||||
}
|
||||
|
||||
static boolean irisWorldsPresent() {
|
||||
try {
|
||||
return irisWorldsPresent(IrisWorldStorage.levelRoot(), ServerProperties.BUKKIT_YML);
|
||||
} catch (Throwable unreadable) {
|
||||
Iris.reportError("Could not inspect this server for Iris worlds; treating it as having them.", unreadable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean irisWorldsPresent(File levelRoot, File bukkitConfiguration) throws IOException {
|
||||
return IrisWorldStorage.hasManagedWorldStorage(levelRoot)
|
||||
|| BukkitWorldConfiguration.configuresLoadingIrisWorld(bukkitConfiguration, levelRoot.toPath());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris;
|
||||
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
||||
@@ -32,10 +33,11 @@ import art.arcane.iris.world.IrisStartupValidation;
|
||||
import art.arcane.iris.world.IrisStartupAdmissionListener;
|
||||
import art.arcane.iris.world.BukkitWorldReconciler;
|
||||
import art.arcane.iris.world.IrisWorldGeneratorResolver;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.PendingWorldDeleteQueue;
|
||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.world.WorldRefusalReporter;
|
||||
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
||||
import art.arcane.iris.update.IrisUpdateNotifier;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||
@@ -131,8 +133,11 @@ import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.server.PluginDisableEvent;
|
||||
import org.bukkit.event.server.PluginEnableEvent;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||
@@ -200,9 +205,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private volatile boolean generatorDrainCompleted;
|
||||
private volatile boolean enableFailed;
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
private volatile boolean papiLifecycleActive;
|
||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
||||
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
||||
@@ -213,9 +220,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||
private BukkitLanguageSwitcher languageSwitcher;
|
||||
private IrisUpdateNotifier updateNotifier;
|
||||
private BukkitDebugDump debugDump;
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile Thread serverLifecycleThread;
|
||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
@@ -613,21 +620,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return plugin == null ? ComponentLog.discriminator("Iris", "&a") : plugin.getTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false when the bootstrap was aborted (unsupported server version); the caller
|
||||
* must bail out of onEnable without touching any further setup.
|
||||
*/
|
||||
private boolean enable() {
|
||||
if (!INMS.isBound()) {
|
||||
Throwable bindFailure = INMS.bindFailure();
|
||||
Iris.error("Iris cannot start: " + (bindFailure == null
|
||||
? "no NMS binding is available for this server version."
|
||||
: bindFailure.getMessage()));
|
||||
// Deferred one tick: disablePlugin from inside onEnable re-enters onDisable
|
||||
// synchronously and the loader then continues registering the half-enabled plugin.
|
||||
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
||||
return false;
|
||||
}
|
||||
private void enable() {
|
||||
EnableTimings timings = new EnableTimings();
|
||||
alreadyDrained.set(false);
|
||||
postStopFinisherStarted.set(false);
|
||||
@@ -649,6 +642,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
||||
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||
updateNotifier = new IrisUpdateNotifier(this);
|
||||
PaperLibBootstrap.install();
|
||||
SimdSupport.install();
|
||||
timings.mark("bootstrap");
|
||||
@@ -788,7 +782,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
});
|
||||
timings.mark("startupTasks");
|
||||
timings.report();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final class EnableTimings {
|
||||
@@ -838,7 +831,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
public void addShutdownHook() {
|
||||
removeShutdownHook();
|
||||
serverLifecycleThread = Thread.currentThread();
|
||||
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||
try {
|
||||
@@ -877,7 +869,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
*/
|
||||
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
||||
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
|
||||
.thenApply(result -> new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message()));
|
||||
.thenApply(this::managedWorldLoadResult);
|
||||
}
|
||||
|
||||
private ManagedWorldLoader.ManagedWorldLoad managedWorldLoadResult(BukkitWorldReconciler.LoadResult result) {
|
||||
if (!result.succeeded() && result.failure() != null) {
|
||||
reportError("Could not explicitly load Iris world " + result.worldKey() + ": " + result.message(), result.failure());
|
||||
}
|
||||
return new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message());
|
||||
}
|
||||
|
||||
public PendingWorldReplacementManager pendingWorldReplacements() {
|
||||
@@ -910,20 +909,25 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
IrisPlatforms.bind(new BukkitPlatform());
|
||||
enableFailed = false;
|
||||
IrisStartupValidation.begin();
|
||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
||||
pendingWorldReplacements.registerPlatformEntryListener();
|
||||
boolean enabled;
|
||||
try {
|
||||
enabled = enable();
|
||||
generatorResolver.refusals().attach(this);
|
||||
IrisPlatforms.bind(new BukkitPlatform());
|
||||
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
||||
pendingWorldReplacements.registerPlatformEntryListener();
|
||||
if (!INMS.isBound()) {
|
||||
refuseUnsupportedServer();
|
||||
return;
|
||||
}
|
||||
enable();
|
||||
} catch (Throwable failure) {
|
||||
refuseVanillaFallback(failure);
|
||||
throw failure;
|
||||
}
|
||||
if (!enabled) {
|
||||
refuseVanillaFallback(null);
|
||||
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||
generatorResolver.refusals().startupWorldsCreated())) {
|
||||
throw failure;
|
||||
}
|
||||
enableFailed = true;
|
||||
return;
|
||||
}
|
||||
BukkitGuiHost.install();
|
||||
@@ -936,57 +940,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||
return;
|
||||
}
|
||||
papiLifecycleActive = true;
|
||||
setupPapi();
|
||||
reportLockedRuntime();
|
||||
}
|
||||
|
||||
private void refuseUnsupportedServer() {
|
||||
Throwable bindFailure = INMS.bindFailure();
|
||||
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
||||
? "no NMS binding is available for this server version."
|
||||
: bindFailure.getMessage()), bindFailure);
|
||||
if (FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||
generatorResolver.refusals().startupWorldsCreated())) {
|
||||
enableFailed = true;
|
||||
return;
|
||||
}
|
||||
Iris.error(failure.getMessage());
|
||||
// Deferred one tick: disablePlugin from inside onEnable re-enters onDisable
|
||||
// synchronously and the loader then continues registering the half-enabled plugin.
|
||||
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
||||
}
|
||||
|
||||
private static void reportLockedRuntime() {
|
||||
String denial = IrisStartupValidation.denialReason().orElse(null);
|
||||
if (denial == null) {
|
||||
return;
|
||||
}
|
||||
boolean managedStorage;
|
||||
try {
|
||||
managedStorage = IrisWorldStorage.hasManagedWorldStorage(IrisWorldStorage.levelRoot());
|
||||
} catch (Throwable unavailable) {
|
||||
Iris.reportError("Could not inspect Iris world storage while reporting the locked runtime.", unavailable);
|
||||
managedStorage = false;
|
||||
}
|
||||
for (String line : RuntimeLockNotice.compose(denial, managedStorage)) {
|
||||
for (String line : RuntimeLockNotice.compose(denial, FailedEnableLock.irisWorldsPresent())) {
|
||||
Iris.error(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a server whose Iris worlds would otherwise be generated by the vanilla generator.
|
||||
* <p>
|
||||
* A disabled Iris gets no {@code getDefaultWorldGenerator} call at all, so the server falls back to
|
||||
* vanilla for every world bukkit.yml points at Iris and writes vanilla terrain into their region files.
|
||||
* There is no Bukkit API that refuses a world at that point, so the server is stopped instead. This is
|
||||
* damage control, not prevention: level creation runs in the same startup step that enables plugins, so
|
||||
* spawn chunks of the affected worlds can still be written before the stop takes effect. The prevention
|
||||
* lives in IrisBootstrap, which refuses startup before any level is created.
|
||||
*/
|
||||
private static void refuseVanillaFallback(Throwable failure) {
|
||||
File levelRoot;
|
||||
try {
|
||||
levelRoot = IrisWorldStorage.levelRoot();
|
||||
} catch (Throwable unavailable) {
|
||||
return;
|
||||
}
|
||||
if (!IrisWorldStorage.hasManagedWorldStorage(levelRoot)) {
|
||||
return;
|
||||
}
|
||||
Iris.error("Iris did not enable and this server has Iris worlds; stopping the server before they generate vanilla terrain.");
|
||||
if (failure != null) {
|
||||
Iris.reportError("Iris enable failed", failure);
|
||||
}
|
||||
try {
|
||||
Bukkit.shutdown();
|
||||
} catch (Throwable unavailable) {
|
||||
Iris.error("Could not stop the server: " + unavailable.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public BukkitDebugDump debugDump() {
|
||||
return debugDump;
|
||||
}
|
||||
@@ -1004,6 +988,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
if (updateNotifier != null) {
|
||||
updateNotifier.close();
|
||||
updateNotifier = null;
|
||||
}
|
||||
if (debugDump != null) {
|
||||
debugDump.close();
|
||||
debugDump = null;
|
||||
@@ -1014,11 +1004,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
IrisLanguage.shutdown();
|
||||
teardownPapi();
|
||||
boolean serverStopping = IrisToolbelt.isServerStopping();
|
||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping());
|
||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||
if (restartingAtStartupBoundary) {
|
||||
teardownRuntime("startup-boundary-restart", 30L);
|
||||
} else if (serverStopping) {
|
||||
} else if (deferToServerStop) {
|
||||
quiesceRuntimeForServerShutdown("onDisable");
|
||||
startPostStopFinisher();
|
||||
} else {
|
||||
@@ -1036,26 +1026,29 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||
super.onDisable();
|
||||
if (!serverStopping || restartingAtStartupBoundary) {
|
||||
if (!deferToServerStop || restartingAtStartupBoundary) {
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
||||
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
teardownPapi();
|
||||
if (IrisToolbelt.isServerStopping()) {
|
||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||
startPostStopFinisher();
|
||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||
return;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
if (alreadyDrained.get()) {
|
||||
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
||||
return;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
||||
drainOnce("pre-unload:" + reason, 45L);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1169,7 +1162,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (generator == null) {
|
||||
continue;
|
||||
}
|
||||
IrisToolbelt.beginWorldMaintenance(world, reason, true);
|
||||
IrisToolbelt.beginWorldMaintenance(world, reason);
|
||||
if (!deferredShutdownGenerators.contains(generator)) {
|
||||
deferredShutdownGenerators.add(generator);
|
||||
}
|
||||
@@ -1181,52 +1174,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
Thread activeServerThread = serverLifecycleThread;
|
||||
if (activeServerThread == null) {
|
||||
Iris.warn("Iris could not start its post-stop runtime finisher because the server lifecycle thread is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
Thread finisher = new Thread(() -> {
|
||||
if (!awaitServerThreadTermination(activeServerThread)) {
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("post-server-stop", 30L);
|
||||
}, "Iris-PostStop-Finisher");
|
||||
Thread finisher = new Thread(() -> finishRuntimeAfterServerShutdown("post-server-stop"),
|
||||
"Iris-PostStop-Finisher");
|
||||
finisher.setDaemon(false);
|
||||
finisher.start();
|
||||
}
|
||||
|
||||
static boolean awaitServerThreadTermination(Thread serverThread) {
|
||||
if (serverThread == null || serverThread == Thread.currentThread()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
serverThread.join();
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Iris.reportError("Iris post-stop runtime finisher was interrupted.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void runShutdownHook() {
|
||||
if (startupBoundaryRestart.get()) {
|
||||
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
||||
return;
|
||||
}
|
||||
finishRuntimeAfterServerShutdown("shutdown-hook");
|
||||
}
|
||||
|
||||
private void finishRuntimeAfterServerShutdown(String reason) {
|
||||
if (!awaitServerShutdownBoundary()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||
SHUTDOWN_ERRORS.println("[Iris] The server did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
||||
finishDeferredRuntimeTeardown(reason, 30L);
|
||||
}
|
||||
|
||||
private boolean awaitServerShutdownBoundary() {
|
||||
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||
if (boundary == null) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return boundary.await(
|
||||
@@ -1290,6 +1263,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
&& preservation.hasActiveResources();
|
||||
}
|
||||
|
||||
private void preservePendingBlockDrops() {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator != null && generator.getEngine() != null
|
||||
&& generator.getEngine().getWorldManager() instanceof IrisWorldManager manager) {
|
||||
manager.flushPendingBlockDropsAsync().whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Iris.reportError("Failed to preserve pending block loot before Iris teardown.", failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||
@@ -1344,8 +1331,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
private void setupPapi() {
|
||||
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
||||
@EventHandler
|
||||
public void onPlaceholderApiEnable(PluginEnableEvent event) {
|
||||
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||
setupPapi();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlaceholderApiDisable(PluginDisableEvent event) {
|
||||
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||
teardownPapi();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void setupPapi() {
|
||||
if (!papiLifecycleActive || papiRegistration != null
|
||||
|| !PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1372,7 +1374,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
papiRegistration = registration;
|
||||
}
|
||||
|
||||
private void teardownPapi() {
|
||||
private synchronized void teardownPapi() {
|
||||
IrisPapiListener listener = papiListener;
|
||||
papiListener = null;
|
||||
|
||||
@@ -1457,6 +1459,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return generatorResolver.resolveDefaultWorldGenerator(worldName, id);
|
||||
}
|
||||
|
||||
public ChunkGenerator requireWorldGenerator(String worldName, String id) {
|
||||
return generatorResolver.requireWorldGenerator(worldName, id);
|
||||
}
|
||||
|
||||
public WorldRefusalReporter worldRefusals() {
|
||||
return generatorResolver.refusals();
|
||||
}
|
||||
|
||||
public void splash() {
|
||||
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
||||
printPacks();
|
||||
@@ -1476,12 +1486,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public int getIrisVersion() {
|
||||
String input = getDescription().getVersion();
|
||||
int hyphenIndex = input.indexOf('-');
|
||||
if (hyphenIndex != -1) {
|
||||
String result = input.substring(0, hyphenIndex);
|
||||
result = result.replaceAll("\\.", "");
|
||||
return Integer.parseInt(result);
|
||||
String release = hyphenIndex < 0 ? input : input.substring(0, hyphenIndex);
|
||||
if (!release.matches("[0-9]+(?:\\.[0-9]+)*")) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(release.replace(".", ""));
|
||||
} catch (NumberFormatException invalid) {
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int getMCVersion() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
|
||||
import io.papermc.paper.ServerBuildInfo;
|
||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
||||
@@ -21,6 +22,7 @@ import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -112,16 +114,18 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
* Stops startup when a world the server is about to load carries Iris storage Iris cannot generate from.
|
||||
* <p>
|
||||
* The server enumerates levels from {@code <levelRoot>/dimensions/<namespace>/<key>} on disk, so a world
|
||||
* whose folder is present is going to be loaded whatever Iris does; and CraftBukkit swallows whatever
|
||||
* {@code getDefaultWorldGenerator} throws and falls back to the vanilla generator, which writes vanilla
|
||||
* terrain straight into that world's region files. A pack snapshot that has gone missing therefore has
|
||||
* to be found here, before any level is created, exactly as a corrupt {@code dimensions/<id>.json} is.
|
||||
* whose folder is present is going to be loaded whatever Iris does. The generator resolver answers a world
|
||||
* it cannot generate with a fail-closed generator, which stops startup at that world's own level creation,
|
||||
* after the levels before it have loaded. A pack snapshot that has gone missing is found here instead,
|
||||
* before any level is created, exactly as a corrupt {@code dimensions/<id>.json} is. An Iris world in a
|
||||
* vanilla slot whose bukkit.yml entry no longer names Iris stops startup too: CraftServer would build it
|
||||
* with the vanilla generator without ever asking Iris.
|
||||
* <p>
|
||||
* A world with no folder at all is not a startup failure: nothing enumerates it, nothing loads it, and
|
||||
* its bukkit.yml and Multiverse entries are what make restoring the folder a complete recovery. Neither
|
||||
* is a folder that holds no pack snapshot and no world data - {@link #quarantineWorthlessHusks} has
|
||||
* already moved those out of the dimensions tree, and one that survives it (a Spigot-layout world folder,
|
||||
* or a move that failed) owns nothing a vanilla generator could destroy. Both are only reported.
|
||||
* or a move that failed) owns nothing to lose. Both are only reported here.
|
||||
*/
|
||||
static void requireUsableWorldStorage(BukkitStartupPaths startupPaths) throws IOException {
|
||||
Path levelRoot = startupPaths.levelRoot();
|
||||
@@ -135,6 +139,7 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
levelRoot
|
||||
);
|
||||
StringBuilder unusable = new StringBuilder();
|
||||
StringBuilder unbound = new StringBuilder();
|
||||
for (IrisWorldStorageEntry entry : entries) {
|
||||
switch (entry.state()) {
|
||||
case PRESENT -> {
|
||||
@@ -152,15 +157,29 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
.append(": ")
|
||||
.append(entry.detail() == null ? "storage is unusable" : entry.detail())
|
||||
.append(')');
|
||||
case UNBOUND -> unbound.append(unbound.isEmpty() ? "" : "; ")
|
||||
.append(entry.configuredWorldName())
|
||||
.append(" (")
|
||||
.append(entry.storagePath())
|
||||
.append("): set worlds.")
|
||||
.append(entry.configuredWorldName())
|
||||
.append(".generator to Iris:<pack> in bukkit.yml, or move the folder aside to start a new"
|
||||
+ " vanilla world there");
|
||||
}
|
||||
}
|
||||
if (unusable.isEmpty()) {
|
||||
return;
|
||||
List<String> refusals = new ArrayList<>(2);
|
||||
if (!unusable.isEmpty()) {
|
||||
refusals.add("Iris world storage is unusable and the server would generate vanilla terrain over it: "
|
||||
+ unusable + ". Restore each world folder from a backup, or move it aside; a world with no"
|
||||
+ " folder is reported and skipped, and the server starts.");
|
||||
}
|
||||
if (!unbound.isEmpty()) {
|
||||
refusals.add("Iris worlds in vanilla slots are not bound to Iris, so the server would generate vanilla"
|
||||
+ " terrain into them: " + unbound + ".");
|
||||
}
|
||||
if (!refusals.isEmpty()) {
|
||||
throw new IllegalStateException(String.join(" ", refusals));
|
||||
}
|
||||
throw new IllegalStateException("Iris world storage is unusable and the server would generate vanilla"
|
||||
+ " terrain over it: " + unusable
|
||||
+ ". Restore each world folder from a backup, or move it aside; a world with no folder is"
|
||||
+ " reported and skipped, and the server starts.");
|
||||
}
|
||||
|
||||
static void armStartupFailure(BootstrapContext context, Throwable failure) {
|
||||
@@ -175,17 +194,30 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
context.getLifecycleManager().registerEventHandler(eventType, event -> {
|
||||
throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure);
|
||||
});
|
||||
List<String> notice = startupRefusalNotice(failure);
|
||||
try {
|
||||
context.getLogger().error(
|
||||
"Iris bootstrap failed; registry and world startup will be stopped at datapack discovery.",
|
||||
failure
|
||||
);
|
||||
for (String line : notice) {
|
||||
context.getLogger().error(line);
|
||||
}
|
||||
context.getLogger().warn("Iris bootstrap failure", failure);
|
||||
} catch (Throwable loggingFailure) {
|
||||
notice.forEach(System.err::println);
|
||||
failure.addSuppressed(loggingFailure);
|
||||
failure.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> startupRefusalNotice(Throwable failure) {
|
||||
return GenerationRefusalNotice.compose(
|
||||
"Iris stopped server startup before any world loads",
|
||||
GenerationRefusalNotice.causes(failure),
|
||||
List.of(
|
||||
"Startup stops at datapack discovery: no world loads and no chunks are written.",
|
||||
"Fix the cause, then start the server again. Paper's --safeMode hint does not apply:"
|
||||
+ " Iris worlds must not load without Iris."
|
||||
));
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
|
||||
try {
|
||||
return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest(
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package art.arcane.iris.api.tree;
|
||||
|
||||
public interface TreeFellerRunHooks {
|
||||
default boolean requiresSneaking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
|
||||
@Override
|
||||
public void onActivationAccepted() {
|
||||
|
||||
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.locator.SubterrainLocator;
|
||||
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
@@ -72,7 +74,7 @@ import java.util.Set;
|
||||
|
||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
@Director(description = "Find a biome", sync = true, descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an authored underground feature by id or geometry family", sync = true)
|
||||
public void subterrain(
|
||||
@Param(description = "Feature definition id or family: tectonic_fault, cenote, lava_tube, travertine_terraces")
|
||||
String feature,
|
||||
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||
int radius,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
String key = feature.trim();
|
||||
IrisSubterrainFamily family = null;
|
||||
try {
|
||||
family = IrisSubterrainFamily.valueOf(key.toUpperCase(Locale.ROOT).replace('-', '_'));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
locateSubterrain(new SubterrainLocator.Query(family == null ? key : "", family, ""), radius, teleport);
|
||||
}
|
||||
|
||||
@Director(name = "underground-biome", description = "Find a bounded authored underground biome", sync = true)
|
||||
public void undergroundBiome(
|
||||
@Param(description = "The underground biome to look for", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||
int radius,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
locateSubterrain(new SubterrainLocator.Query("", null, biome.getLoadKey()), radius, teleport);
|
||||
}
|
||||
|
||||
private void locateSubterrain(SubterrainLocator.Query query, int radius, boolean teleport) {
|
||||
Engine activeEngine = engine();
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = player();
|
||||
if (activeEngine == null || commandSender == null || target == null) {
|
||||
if (commandSender != null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (radius < 0 || radius > 32768) {
|
||||
commandSender.sendMessage(C.RED + "Search radius must be between 0 and 32768 blocks.");
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
World world = origin.getWorld();
|
||||
commandSender.sendMessage(C.GRAY + "Searching authored underground plans...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(activeEngine, query,
|
||||
origin.getBlockX(), origin.getBlockY(), origin.getBlockZ(), radius).orElse(null);
|
||||
if (found == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No matching authored underground volume found within " + radius + " blocks.");
|
||||
return;
|
||||
}
|
||||
String label = found.family().name().toLowerCase(Locale.ROOT) + " " + found.featureId();
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender, C.GREEN + "Found " + label + " at "
|
||||
+ found.x() + ", " + found.y() + ", " + found.z() + ".");
|
||||
return;
|
||||
}
|
||||
prepareStructureTeleport(target, world, commandSender, label,
|
||||
new Location(world, found.x(), found.y(), found.z()), true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate authored underground volume: " + error.getMessage());
|
||||
Iris.reportError("Could not locate authored underground volume.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||
|
||||
@@ -511,7 +511,9 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
||||
// Maintenance that forbids mantle stages refuses every chunk, and Folia treats a refused chunk as a chunk
|
||||
// system failure, so it starts only once no player is left to request one.
|
||||
AtomicBoolean maintenanceEntered = new AtomicBoolean(false);
|
||||
try {
|
||||
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
||||
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
||||
@@ -523,6 +525,8 @@ public class CommandIris implements DirectorExecutor {
|
||||
if (!Boolean.TRUE.equals(evacuated)) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
||||
maintenanceEntered.set(true);
|
||||
return WorldLifecycleService.get().unloadAsync(world, true);
|
||||
})
|
||||
.thenCompose(unloaded -> {
|
||||
@@ -537,7 +541,9 @@ public class CommandIris implements DirectorExecutor {
|
||||
});
|
||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||
.whenComplete((unloaded, throwable) -> {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
if (maintenanceEntered.get()) {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
}
|
||||
lease.close();
|
||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||
@@ -546,7 +552,9 @@ public class CommandIris implements DirectorExecutor {
|
||||
J.s(response);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
if (maintenanceEntered.get()) {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
}
|
||||
lease.close();
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
@@ -174,7 +176,6 @@ public class CommandPack implements DirectorExecutor {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||
return;
|
||||
@@ -230,7 +231,6 @@ public class CommandPack implements DirectorExecutor {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
||||
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||
return;
|
||||
@@ -363,47 +363,79 @@ public class CommandPack implements DirectorExecutor {
|
||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||
}
|
||||
|
||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
PackValidationRegistry.ValidationTicket ticket =
|
||||
PackValidationRegistry.tryBeginValidation(packFolder.toPath());
|
||||
PackValidationResult result;
|
||||
String contentFingerprint = "";
|
||||
String contextFingerprint = "";
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packFolder);
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
if (ticket == null) {
|
||||
throw new IllegalStateException("Pack content is currently being changed.");
|
||||
}
|
||||
contentFingerprint = PackFingerprints.computePackTreeFingerprint(packFolder);
|
||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
result = PackValidator.validate(packFolder);
|
||||
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packFolder))
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
throw new IllegalStateException("Pack content or validation context changed during validation.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||
? e.getClass().getSimpleName()
|
||||
: e.getMessage();
|
||||
PackValidationResult result = new PackValidationResult(
|
||||
result = new PackValidationResult(
|
||||
packFolder.getName(),
|
||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
contentFingerprint = "";
|
||||
contextFingerprint = "";
|
||||
}
|
||||
if (PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||
PackValidationRegistry.publish(result);
|
||||
} else {
|
||||
result = new PackValidationResult(packFolder.getName(),
|
||||
List.of("Pack content changed during validation."), List.of(), System.currentTimeMillis());
|
||||
}
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void persistValidationCache(File packsRoot) {
|
||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||
for (File packDirectory : packDirectories) {
|
||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
||||
if (result == null) {
|
||||
// The boot cache only loads when it covers every pack, so a partial write is
|
||||
// pointless - but say so instead of silently skipping the persist forever.
|
||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
||||
try {
|
||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
PackFingerprints.PackContentSnapshot snapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||
for (File packDirectory : packDirectories) {
|
||||
PackValidationResult result = PackValidationRegistry.getMatching(
|
||||
packDirectory.toPath(), snapshot.packContents().get(packDirectory.getName()));
|
||||
if (result == null) {
|
||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||
+ "\" has no validation matching its current content. Run /iris pack validate to refresh it.");
|
||||
return;
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
if (!snapshot.content().equals(PackFingerprints.computePackContentSnapshot(packsRoot).content())
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
Iris.warn("Pack validation cache not written because pack content or validation context changed.");
|
||||
return;
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
try {
|
||||
for (int index = 0; index < packDirectories.size(); index++) {
|
||||
File packDirectory = packDirectories.get(index);
|
||||
if (PackValidationRegistry.getMatching(packDirectory.toPath(),
|
||||
snapshot.packContents().get(packDirectory.getName())) != results.get(index)) {
|
||||
Iris.warn("Pack validation cache not written because validation changed during persistence.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
PackValidationCache.save(
|
||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||
PackValidationCache.contentFingerprint(packsRoot),
|
||||
PackValidationCache.contextFingerprint(),
|
||||
snapshot.content(),
|
||||
contextFingerprint,
|
||||
results);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
||||
|
||||
@@ -81,7 +81,6 @@ import org.bukkit.FluidCollisionMode;
|
||||
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.Vector;
|
||||
|
||||
@@ -309,23 +308,33 @@ public class CommandStudio implements DirectorExecutor {
|
||||
O<Integer> ta = new O<>();
|
||||
ta.set(-1);
|
||||
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
VolmitSender sender = sender();
|
||||
Player player = player();
|
||||
Engine engine = engine();
|
||||
|
||||
ta.set(J.sr(() ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
ta.set(J.sr(() -> {
|
||||
if (!J.runEntity(player, () -> {
|
||||
if (player.getOpenInventory().getTopInventory() != inv) {
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
||||
return;
|
||||
}
|
||||
if (!add) {
|
||||
inv.clear();
|
||||
}
|
||||
Location location = player.getLocation();
|
||||
try {
|
||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE,
|
||||
location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
} catch (RuntimeException | Error failure) {
|
||||
J.csr(ta.get());
|
||||
Iris.reportError(failure);
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE,
|
||||
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
|
||||
}
|
||||
}, 0, () -> J.csr(ta.get()))) {
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!add) {
|
||||
inv.clear();
|
||||
}
|
||||
|
||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
|
||||
}, fast ? 5 : 35));
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
final class GenerationRateWindow {
|
||||
static final long TIMING_IDLE_MILLIS = 10_000L;
|
||||
|
||||
private long lastSampleAtMs;
|
||||
private int lastGeneratedCount;
|
||||
private long lastGenerationAtMs;
|
||||
|
||||
double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
|
||||
int safeGeneratedCount = Math.max(0, generatedCount);
|
||||
long previousAt = lastSampleAtMs;
|
||||
int previousCount = lastGeneratedCount;
|
||||
lastSampleAtMs = sampledAtMs;
|
||||
lastGeneratedCount = safeGeneratedCount;
|
||||
if (previousAt <= 0L) {
|
||||
if (safeGeneratedCount > 0) {
|
||||
lastGenerationAtMs = sampledAtMs;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
if (sampledAtMs <= previousAt) {
|
||||
return 0D;
|
||||
}
|
||||
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
|
||||
if (generatedDelta > 0) {
|
||||
lastGenerationAtMs = sampledAtMs;
|
||||
}
|
||||
return generatedDelta * 1000D / (sampledAtMs - previousAt);
|
||||
}
|
||||
|
||||
boolean generationTimingsFresh(long sampledAtMs) {
|
||||
return lastGenerationAtMs > 0L && sampledAtMs - lastGenerationAtMs <= TIMING_IDLE_MILLIS;
|
||||
}
|
||||
}
|
||||
+8
-17
@@ -460,8 +460,12 @@ public final class IrisEngineSVC implements IrisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now));
|
||||
double chunksPerSecond = registered.generationRate.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(
|
||||
engine,
|
||||
chunksPerSecond,
|
||||
registered.generationRate.generationTimingsFresh(now),
|
||||
now));
|
||||
registered.clearMetricsFailure();
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
@@ -552,6 +556,7 @@ public final class IrisEngineSVC implements IrisService {
|
||||
progress.generated(),
|
||||
progress.totalChunks(),
|
||||
progress.chunksRemaining(),
|
||||
progress.chunksInFlight(),
|
||||
progress.chunksPerSecond(),
|
||||
progress.eta(),
|
||||
progress.elapsed(),
|
||||
@@ -612,8 +617,7 @@ public final class IrisEngineSVC implements IrisService {
|
||||
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
|
||||
private volatile ScheduledFuture<?> maintenance;
|
||||
private volatile boolean closed;
|
||||
private long lastGeneratedSampleAtMs;
|
||||
private int lastGeneratedCount;
|
||||
private final GenerationRateWindow generationRate = new GenerationRateWindow();
|
||||
|
||||
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
|
||||
name = registration.name();
|
||||
@@ -696,19 +700,6 @@ public final class IrisEngineSVC implements IrisService {
|
||||
return activeMaintenance.get() != null;
|
||||
}
|
||||
|
||||
private double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
|
||||
int safeGeneratedCount = Math.max(0, generatedCount);
|
||||
long previousAt = lastGeneratedSampleAtMs;
|
||||
int previousCount = lastGeneratedCount;
|
||||
lastGeneratedSampleAtMs = sampledAtMs;
|
||||
lastGeneratedCount = safeGeneratedCount;
|
||||
if (previousAt <= 0L || sampledAtMs <= previousAt) {
|
||||
return 0D;
|
||||
}
|
||||
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
|
||||
return generatedDelta * 1000D / (sampledAtMs - previousAt);
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
|
||||
+3
@@ -101,6 +101,7 @@ public record IrisTelemetrySnapshot(
|
||||
long generated,
|
||||
long total,
|
||||
long remaining,
|
||||
long inFlight,
|
||||
double chunksPerSecond,
|
||||
long etaMs,
|
||||
long elapsedMs,
|
||||
@@ -115,6 +116,7 @@ public record IrisTelemetrySnapshot(
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0D,
|
||||
0L,
|
||||
0L,
|
||||
@@ -130,6 +132,7 @@ public record IrisTelemetrySnapshot(
|
||||
generated = Math.max(0L, generated);
|
||||
total = Math.max(0L, total);
|
||||
remaining = Math.max(0L, remaining);
|
||||
inFlight = Math.max(0L, inFlight);
|
||||
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||
etaMs = Math.max(0L, etaMs);
|
||||
elapsedMs = Math.max(0L, elapsedMs);
|
||||
|
||||
+32
-2
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||
@@ -13,6 +14,9 @@ import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricPublisher;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
|
||||
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||
@@ -30,7 +34,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
||||
public class IrisIntegrationService implements IrisService, IntegrationSnapshotProvider {
|
||||
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||
new IntegrationProtocolVersion(1, 0),
|
||||
@@ -41,6 +45,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
"handshake",
|
||||
"heartbeat",
|
||||
"metrics",
|
||||
IntegrationSnapshotProvider.CAPABILITY,
|
||||
"metric-groups",
|
||||
"iris-engine-metrics",
|
||||
"iris-world-metrics"
|
||||
@@ -63,6 +68,9 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
);
|
||||
|
||||
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||
private final IntegrationMetricPublisher snapshots = new IntegrationMetricPublisher(
|
||||
IntegrationMetricSchema.irisKeys().size(), 30_000L);
|
||||
private int snapshotTaskId = -1;
|
||||
|
||||
public IrisIntegrationService() {
|
||||
this(IrisIntegrationService::currentTelemetry);
|
||||
@@ -77,11 +85,17 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||
snapshotTaskId = J.ar(this::publishSnapshots, 20);
|
||||
Iris.verbose("Integration provider registered for Iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (snapshotTaskId != -1) {
|
||||
J.car(snapshotTaskId);
|
||||
snapshotTaskId = -1;
|
||||
}
|
||||
snapshots.clear();
|
||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||
}
|
||||
|
||||
@@ -188,6 +202,20 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
return Map.copyOf(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationMetricSnapshot snapshotMetrics(Set<String> metricKeys) {
|
||||
return snapshots.snapshotMetrics(metricKeys, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
void publishSnapshots() {
|
||||
IntegrationMetricPublisher.Demand demand = snapshots.demandedKeys(System.currentTimeMillis());
|
||||
if (demand.keys().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, IntegrationMetricSample> samples = sampleMetrics(demand.keys());
|
||||
snapshots.publish(demand, System.currentTimeMillis(), samples);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IntegrationMetricGroup> metricGroups() {
|
||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||
@@ -319,6 +347,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_REMAINING,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
|
||||
@@ -332,7 +361,8 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.inFlight(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_REMAINING, pregenerator.remaining(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.update;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.volmlib.util.update.BukkitUpdateService;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class IrisUpdateNotifier implements AutoCloseable {
|
||||
private final Consumer<IrisSettings> settingsListener = this::reconfigure;
|
||||
private final BukkitUpdateService updates;
|
||||
private volatile boolean enabled;
|
||||
private boolean closed;
|
||||
|
||||
public IrisUpdateNotifier(Plugin plugin) {
|
||||
enabled = IrisSettings.get().getGeneral().isUpdateNotifications();
|
||||
updates = BukkitUpdateService.register(plugin, new BukkitUpdateService.Options(
|
||||
"VolmitSoftware", "Iris", "iris.update", () -> enabled));
|
||||
IrisSettings.addSettingsListener(settingsListener);
|
||||
reconfigure(IrisSettings.get());
|
||||
}
|
||||
|
||||
private synchronized void reconfigure(IrisSettings settings) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
enabled = settings.getGeneral().isUpdateNotifications();
|
||||
updates.reconfigure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
closed = true;
|
||||
IrisSettings.removeSettingsListener(settingsListener);
|
||||
updates.close();
|
||||
}
|
||||
}
|
||||
+18
-7
@@ -722,19 +722,30 @@ public final class BukkitWorldReconciler {
|
||||
return WorldIdentity.resolve(worldKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* A world Iris cannot resolve is refused like one CraftServer asked for, with the banner and operator notice,
|
||||
* then fails this load only.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed) {
|
||||
String configuredWorldName = worldKey.toString();
|
||||
ChunkGenerator generator;
|
||||
IrisDimension irisDimension;
|
||||
try {
|
||||
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
|
||||
String configuredWorldName = configuredWorldName(worldKey);
|
||||
Iris.info("Loading World: %s | Generator: %s", logicalWorldName, dimension);
|
||||
ChunkGenerator generator = plugin.getDefaultWorldGenerator(configuredWorldName, dimension);
|
||||
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
||||
configuredWorldName = configuredWorldName(worldKey);
|
||||
Iris.info("Loading World: %s | Generator: %s", IrisWorldStorage.logicalName(worldKey), dimension);
|
||||
generator = plugin.requireWorldGenerator(configuredWorldName, dimension);
|
||||
irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
||||
if (generator == null || irisDimension == null) {
|
||||
throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\".");
|
||||
}
|
||||
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + logicalWorldName + " using Iris:" + dimension + "...");
|
||||
} catch (Throwable refusal) {
|
||||
plugin.worldRefusals().report(configuredWorldName, worldKey, refusal);
|
||||
return CompletableFuture.failedFuture(refusal);
|
||||
}
|
||||
try {
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + IrisWorldStorage.logicalName(worldKey)
|
||||
+ " using Iris:" + dimension + "...");
|
||||
WorldCreator creator = WorldCreatorCompat.ofPersistentKey(worldKey)
|
||||
.generator(generator)
|
||||
.environment(BukkitEnvironment.from(irisDimension.getEnvironment()));
|
||||
|
||||
+15
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
@@ -48,6 +49,20 @@ final class IrisFailClosedChunkGenerator extends ChunkGenerator {
|
||||
+ "' remains locked: " + denialReason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Built on the way back to CraftServer, which turns any throw into the vanilla generator, so a cause that cannot
|
||||
* be summarized is named by its type instead.
|
||||
*/
|
||||
static IrisFailClosedChunkGenerator refused(String worldName, Throwable failure) {
|
||||
String reason;
|
||||
try {
|
||||
reason = GenerationRefusalNotice.summary(failure);
|
||||
} catch (Throwable unreadable) {
|
||||
reason = failure.getClass().getName();
|
||||
}
|
||||
return new IrisFailClosedChunkGenerator("Iris refuses to generate '" + worldName + "': " + reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
|
||||
throw refusal();
|
||||
|
||||
+101
-26
@@ -18,8 +18,9 @@
|
||||
|
||||
package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
||||
@@ -73,6 +74,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
|
||||
@@ -90,6 +92,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
private final AtomicBoolean externalContentRefreshQueued = new AtomicBoolean();
|
||||
private final AtomicBoolean externalContentRefreshRequested = new AtomicBoolean();
|
||||
private final Map<Path, PendingSnapshot> pendingSnapshots = new ConcurrentHashMap<>();
|
||||
private final WorldRefusalReporter refusals = new WorldRefusalReporter();
|
||||
|
||||
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -182,12 +185,12 @@ public final class IrisWorldGeneratorResolver {
|
||||
}
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
PackFingerprints.PackContentSnapshot contentSnapshot =
|
||||
new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||
String contextFingerprint = "";
|
||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||
try {
|
||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
contentSnapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
if (!externalContentChanged) {
|
||||
cached = PackValidationCache.load(
|
||||
@@ -272,19 +275,19 @@ public final class IrisWorldGeneratorResolver {
|
||||
private static FreshValidation validateStablePacks(
|
||||
File packsRoot,
|
||||
List<File> initialPackDirs,
|
||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
||||
PackFingerprints.PackContentSnapshot initialSnapshot
|
||||
) {
|
||||
List<File> packDirs = initialPackDirs;
|
||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
||||
PackFingerprints.PackContentSnapshot before = initialSnapshot;
|
||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
List<PackValidationResult> results = validatePacks(packDirs);
|
||||
ServerConfigurator.PackContentSnapshot after;
|
||||
PackFingerprints.PackContentSnapshot after;
|
||||
try {
|
||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
after = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
||||
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
after = new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||
}
|
||||
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||
@@ -480,12 +483,28 @@ public final class IrisWorldGeneratorResolver {
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The generator CraftServer asks for. Paper turns a throw or a null here into the vanilla generator, so every
|
||||
* request aimed at an Iris world gets a generator back: the Iris one, or a fail-closed one that refuses to let
|
||||
* the level be created. Only the PlotSquared discovery sentinel, which is never created, gets null.
|
||||
*/
|
||||
@Nullable
|
||||
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, @Nullable String id) {
|
||||
if (isPlotSquaredGeneratorDiscoveryProbe(worldName, id)) {
|
||||
Iris.debug("Ignoring PlotSquared generator discovery probe");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return requireWorldGenerator(worldName, id);
|
||||
} catch (Throwable failure) {
|
||||
return refuseWorld(worldName, failure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolution for Iris' own loads, which report a failure per world instead of handing it to CraftServer.
|
||||
*/
|
||||
public ChunkGenerator requireWorldGenerator(String worldName, @Nullable String id) {
|
||||
if (isGeneratorDiscoveryProbe(worldName, id)) {
|
||||
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
||||
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
||||
@@ -505,21 +524,37 @@ public final class IrisWorldGeneratorResolver {
|
||||
return startupLock;
|
||||
}
|
||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||
String dimension = id == null || id.isEmpty() ? IrisSettings.get().getGenerator().getDefaultWorldType() : id;
|
||||
Iris.debug("Generator ID: " + dimension + " requested by bukkit/plugin");
|
||||
|
||||
File levelRoot = IrisWorldStorage.levelRoot();
|
||||
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
||||
requireWorldKeyAvailable(worldName, worldKey);
|
||||
requireOwnedWorld(worldName, levelRoot, worldKey);
|
||||
return resolveFrozenWorldGenerator(worldName, dimension);
|
||||
}
|
||||
|
||||
public WorldRefusalReporter refusals() {
|
||||
return refusals;
|
||||
}
|
||||
|
||||
private ChunkGenerator refuseWorld(String worldName, Throwable failure) {
|
||||
refusals.report(worldName, refusalKey(worldName), failure);
|
||||
try {
|
||||
return resolveFrozenWorldGenerator(worldName, id);
|
||||
} catch (RuntimeException failure) {
|
||||
Iris.reportError("Refusing to load configured Iris world '" + worldName
|
||||
+ "' because its frozen world-local pack snapshot could not be used.", failure);
|
||||
Bukkit.shutdown();
|
||||
throw failure;
|
||||
Iris.reportError(failure);
|
||||
} catch (Throwable loggingFailure) {
|
||||
System.err.println("[Iris] Could not log the refusal of '" + worldName + "': "
|
||||
+ loggingFailure.getClass().getName());
|
||||
}
|
||||
return IrisFailClosedChunkGenerator.refused(worldName, failure);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static NamespacedKey refusalKey(String worldName) {
|
||||
try {
|
||||
return messageWorldKey(worldName, IrisWorldStorage.levelRoot().getName());
|
||||
} catch (Throwable unresolved) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,12 +623,56 @@ public final class IrisWorldGeneratorResolver {
|
||||
return;
|
||||
}
|
||||
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
|
||||
if (!IRIS_DIMENSION_NAMESPACE.equals(messageKey.getNamespace())) {
|
||||
if (dimensionRoot != null && IrisWorldStorage.holdsIrisContent(dimensionRoot.toPath())) {
|
||||
throw new IllegalStateException("'" + worldName + "' (" + messageKey + ") is an Iris world whose"
|
||||
+ " generation history is missing from "
|
||||
+ GenerationHistoryPaths.forDimension(dimensionRoot.toPath()).generationRoot()
|
||||
+ ", so Iris cannot generate it. Restore that folder from a backup. Keep worlds." + worldName
|
||||
+ ".generator in bukkit.yml: without it the server generates vanilla terrain into this world.");
|
||||
}
|
||||
// Only bukkit.yml binds a vanilla slot at startup, and the refusal stops startup, so no command can run
|
||||
// until that binding is gone.
|
||||
throw new IllegalStateException("'" + worldName + "' (" + messageKey
|
||||
+ ") is a vanilla world slot with no Iris world storage, so Iris cannot generate it."
|
||||
+ " Remove worlds." + worldName + ".generator from bukkit.yml"
|
||||
+ leftoverLevelDataRemoval(levelRoot)
|
||||
+ ", start the server, then run /iris replace " + messageKey
|
||||
+ " type=<pack> to replace it with Iris on the next restart.");
|
||||
}
|
||||
throw new IllegalStateException("'" + worldName + "' (" + messageKey
|
||||
+ ") has no Iris world storage, so Iris cannot generate it."
|
||||
+ " Create Iris worlds with /iris create " + IrisWorldStorage.logicalName(messageKey)
|
||||
+ " type=<pack>; Iris registers them with Multiverse itself.");
|
||||
}
|
||||
|
||||
/**
|
||||
* A start refused before the level's overworld existed still leaves level.dat behind, and vanilla cannot start
|
||||
* from a level.dat without world generation settings. A level that holds no chunks loses nothing to its removal.
|
||||
*/
|
||||
private static String leftoverLevelDataRemoval(File levelRoot) {
|
||||
Path level = levelRoot.toPath();
|
||||
Path overworld = level.resolve("dimensions").resolve("minecraft").resolve("overworld");
|
||||
if (Files.exists(overworld.resolve("data").resolve("minecraft").resolve("world_gen_settings.dat"),
|
||||
LinkOption.NOFOLLOW_LINKS)
|
||||
|| hasEntries(overworld.resolve("region"))
|
||||
|| hasEntries(level.resolve("region"))) {
|
||||
return "";
|
||||
}
|
||||
return " and delete " + level.resolve("level.dat") + " (this level has no chunks and was never created)";
|
||||
}
|
||||
|
||||
private static boolean hasEntries(Path directory) {
|
||||
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return false;
|
||||
}
|
||||
try (Stream<Path> entries = Files.list(directory)) {
|
||||
return entries.findAny().isPresent();
|
||||
} catch (IOException unreadable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasGenerationStorage(File dimensionRoot) {
|
||||
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
|
||||
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|
||||
@@ -817,14 +896,10 @@ public final class IrisWorldGeneratorResolver {
|
||||
}
|
||||
|
||||
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
|
||||
return IrisPlatforms.get()
|
||||
.registries()
|
||||
.generationRegistry()
|
||||
.dimensionTypeResourceKey(
|
||||
data.getDataFolder().getName(),
|
||||
dimension.getLoadKey(),
|
||||
dimension.getDimensionTypeKey()
|
||||
);
|
||||
return dimension.dimensionTypeResourceKey(
|
||||
IrisPlatforms.get().registries().generationRegistry(),
|
||||
data.getDataFolder().getName()
|
||||
);
|
||||
}
|
||||
|
||||
private record LoadedHistory(GenerationHistory history, File activePack) {
|
||||
@@ -845,7 +920,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
|
||||
private record FreshValidation(
|
||||
List<File> packDirs,
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
||||
PackFingerprints.PackContentSnapshot contentSnapshot,
|
||||
List<PackValidationResult> results,
|
||||
boolean stable
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.world;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
|
||||
/**
|
||||
* Reports Iris worlds that refused to generate: a console banner that says what the refusal does to the server, and
|
||||
* an operator notice on join for as long as the world stays unloaded, since the banner scrolls away.
|
||||
*/
|
||||
public final class WorldRefusalReporter implements Listener {
|
||||
private static final String UNRESOLVED_KEY = "unresolved key";
|
||||
|
||||
private final Map<String, String> operatorNotices = new ConcurrentSkipListMap<>();
|
||||
private volatile boolean startupWorldsCreated;
|
||||
|
||||
/**
|
||||
* Iris enabled on a running server (a hotload) finds the startup worlds already loaded.
|
||||
*/
|
||||
public void attach(Plugin plugin) {
|
||||
if (!Bukkit.getWorlds().isEmpty()) {
|
||||
startupWorldsCreated = true;
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paper creates every startup level before it prepares any, and preparing one fires the first WorldLoadEvent.
|
||||
* A generator asked for after that belongs to a runtime create, which fails only its caller.
|
||||
*/
|
||||
public boolean startupWorldsCreated() {
|
||||
return startupWorldsCreated;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
startupWorldsCreated = true;
|
||||
try {
|
||||
operatorNotices.remove(WorldIdentity.key(event.getWorld()).toString());
|
||||
} catch (RuntimeException unkeyed) {
|
||||
Iris.debug("Loaded world " + event.getWorld().getName() + " has no key to clear a refusal notice by");
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if (operatorNotices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
if (!player.isOp() && !player.hasPermission("iris.all")) {
|
||||
return;
|
||||
}
|
||||
for (String notice : operatorNotices.values()) {
|
||||
player.sendMessage(C.RED + notice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Never throws: the caller is handing CraftServer a fail-closed generator or failing a load, and a reporting
|
||||
* failure must not turn either into something else.
|
||||
*/
|
||||
public void report(String worldName, @Nullable NamespacedKey worldKey, Throwable failure) {
|
||||
String key = worldKey == null ? UNRESOLVED_KEY : worldKey.toString();
|
||||
try {
|
||||
for (String line : notice(worldName, key, failure, startupWorldsCreated)) {
|
||||
Iris.error(line);
|
||||
}
|
||||
operatorNotices.put(worldKey == null ? worldName : key, "Iris world '" + worldName + "' (" + key
|
||||
+ ") did not load and refuses to generate: " + GenerationRefusalNotice.summary(failure));
|
||||
} catch (Throwable reportingFailure) {
|
||||
System.err.println("[Iris] Iris refused to generate world '" + worldName + "' (" + key
|
||||
+ "); the refusal could not be reported: " + reportingFailure.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> notice(String worldName, String worldKey, Throwable failure, boolean startupWorldsCreated) {
|
||||
return GenerationRefusalNotice.compose(
|
||||
"Iris refused to generate world '" + worldName + "' (" + worldKey + ")",
|
||||
GenerationRefusalNotice.causes(failure),
|
||||
List.of(
|
||||
"Iris does not generate this world and does not let vanilla or any other generator"
|
||||
+ " write it; no chunks are written.",
|
||||
startupWorldsCreated
|
||||
? "The world does not load; the server keeps running. Fix the cause, then load it again."
|
||||
: "Server startup stops before this world loads. Fix the cause, then start the server again."
|
||||
));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -189,7 +189,7 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBypassMantleStages(Engine engine) {
|
||||
public boolean shouldRefuseGeneration(Engine engine) {
|
||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+18
-4
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||
private final TreeFellerSVC service;
|
||||
private final FellingRun run;
|
||||
private final ItemStack tool;
|
||||
|
||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
||||
super(block, player);
|
||||
this.run = run;
|
||||
this.service = service;
|
||||
RoutedBlockBreakEvent(Options options) {
|
||||
super(options.block(), options.player());
|
||||
run = options.run();
|
||||
service = options.service();
|
||||
tool = Objects.requireNonNull(options.tool(), "tool").clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ItemStack> captureNativeDrops() {
|
||||
return getBlock().getDrops(tool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean routeDrop(Object drop) {
|
||||
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
||||
}
|
||||
|
||||
record Options(Block block, Player player, FellingRun run, TreeFellerSVC service, ItemStack tool) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
|
||||
if (!event.isSneaking()) {
|
||||
runner.finishRuns(event.getPlayer().getUniqueId());
|
||||
runner.finishSneakRuns(event.getPlayer().getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-5
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||
block, run.candidate.player(), run, service, reservation.toolForDrops()));
|
||||
service.managedEvents.add(probe);
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(probe);
|
||||
@@ -523,10 +524,15 @@ final class TreeFellingRunner {
|
||||
}
|
||||
|
||||
private boolean isRunControlActive(FellingRun run, Player player) {
|
||||
return player.isOnline()
|
||||
&& player.getGameMode() == GameMode.SURVIVAL
|
||||
&& player.isSneaking()
|
||||
&& player.getWorld().equals(run.candidate.world());
|
||||
try {
|
||||
return player.isOnline()
|
||||
&& player.getGameMode() == GameMode.SURVIVAL
|
||||
&& (!run.runHooks.requiresSneaking() || player.isSneaking())
|
||||
&& player.getWorld().equals(run.candidate.world());
|
||||
} catch (RuntimeException error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration control check failed.", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean reserveLogCost(FellingRun run) {
|
||||
@@ -622,6 +628,24 @@ final class TreeFellingRunner {
|
||||
}
|
||||
}
|
||||
|
||||
void finishSneakRuns(UUID playerId) {
|
||||
Set<FellingRun> runs = service.activeRuns.get(playerId);
|
||||
if (runs == null) {
|
||||
return;
|
||||
}
|
||||
for (FellingRun run : List.copyOf(runs)) {
|
||||
try {
|
||||
if (!run.runHooks.requiresSneaking()) {
|
||||
continue;
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration control check failed.", error);
|
||||
}
|
||||
run.abortReason = "halted";
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
|
||||
List<TreeMarkerTraversal.Position> positions
|
||||
) {
|
||||
|
||||
@@ -10,6 +10,9 @@ authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: VolmitSoftware.com
|
||||
description: More than a Dimension!
|
||||
permissions:
|
||||
iris.update:
|
||||
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||
default: op
|
||||
iris.debugdump:
|
||||
description: Allows creating and optionally uploading diagnostic reports.
|
||||
default: op
|
||||
@@ -25,34 +28,26 @@ permissions:
|
||||
dependencies:
|
||||
server:
|
||||
PlaceholderAPI:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
CraftEngine:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
Nexo:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
Oraxen:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
ItemsAdder:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
SCore:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ExecutableItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: false
|
||||
MythicLib:
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
@@ -61,11 +56,7 @@ dependencies:
|
||||
required: false
|
||||
join-classpath: true
|
||||
eco:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
EcoItems:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicMobs:
|
||||
@@ -77,7 +68,7 @@ dependencies:
|
||||
required: false
|
||||
join-classpath: true
|
||||
KGenerators:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
Multiverse-Core:
|
||||
@@ -85,6 +76,6 @@ dependencies:
|
||||
required: false
|
||||
join-classpath: true
|
||||
WorldEdit:
|
||||
load: BEFORE
|
||||
load: OMIT
|
||||
required: false
|
||||
join-classpath: true
|
||||
|
||||
@@ -3,18 +3,6 @@ version: ${version}
|
||||
main: ${main}
|
||||
folia-supported: true
|
||||
load: STARTUP
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
- CraftEngine
|
||||
- Nexo
|
||||
- Oraxen
|
||||
- ItemsAdder
|
||||
- SCore
|
||||
- ExecutableItems
|
||||
- eco
|
||||
- EcoItems
|
||||
- KGenerators
|
||||
- WorldEdit
|
||||
loadbefore: [ Multiverse-Core ]
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: VolmitSoftware.com
|
||||
@@ -23,6 +11,9 @@ commands:
|
||||
iris:
|
||||
aliases: [ ir, irs ]
|
||||
permissions:
|
||||
iris.update:
|
||||
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||
default: op
|
||||
iris.debugdump:
|
||||
description: Allows creating and optionally uploading diagnostic reports.
|
||||
default: op
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:oak_stairs",
|
||||
"data": {
|
||||
"half": "bottom",
|
||||
"facing": "north"
|
||||
}
|
||||
}
|
||||
],
|
||||
"chance": 1,
|
||||
"forcePlace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Decorator Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-decorators',
|
||||
description: 'Verify authored stair decorations occupy one block and retain their half state.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Decorator generation requires an isolated instance')
|
||||
await context.step('create a world with authored stair decorations', async () => {
|
||||
const response = await context.command('/iris create name=decorator_probe type=decorator-fixture seed=78264193',
|
||||
/Successfully created your world|Failed to create/, 180000)
|
||||
context.expect(response.includes('Successfully created your world'), response)
|
||||
})
|
||||
await context.step('client sees single bottom-half stair decorations', async () => {
|
||||
let checked = 0
|
||||
await context.waitUntil(() => {
|
||||
checked = 0
|
||||
const center = context.bot.entity.position.floored()
|
||||
for (let x = -6; x <= 6; x++) {
|
||||
for (let z = -6; z <= 6; z++) {
|
||||
for (let y = -4; y <= 1; y++) {
|
||||
const position = center.offset(x, y, z)
|
||||
if (context.bot.blockAt(position)?.name !== 'lime_wool') continue
|
||||
const decoration = context.bot.blockAt(position.offset(0, 1, 0))
|
||||
const above = context.bot.blockAt(position.offset(0, 2, 0))
|
||||
if (!decoration || !above) return false
|
||||
if (decoration.name !== 'oak_stairs' || decoration.getProperties().half !== 'bottom'
|
||||
|| above.name !== 'air') return false
|
||||
checked++
|
||||
}
|
||||
}
|
||||
}
|
||||
return checked >= 100
|
||||
}, { label: 'At least 100 authored stair columns with empty space above', timeoutMs: 60000, intervalMs: 250 })
|
||||
context.expect(checked >= 100, 'Client verified generated decorator columns', { checked })
|
||||
context.report.decoratorColumns = checked
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ name: IrisNativeLifecycleFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris]
|
||||
commands:
|
||||
irislifecycle:
|
||||
|
||||
+284
-7
@@ -1,11 +1,31 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.WorldCreatorCompat;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.loot.Lootable;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
||||
@@ -19,6 +39,10 @@ import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
@@ -26,12 +50,16 @@ import java.util.logging.Level;
|
||||
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
||||
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
|
||||
private static final NamespacedKey TREASURE_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||
private static final long WORLD_SEED = 78264193L;
|
||||
private NativeWorldRuntime runtime;
|
||||
private TreasureLootChecks treasureLootChecks;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
||||
treasureLootChecks = new TreasureLootChecks(this);
|
||||
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
||||
getLogger().info("Native lifecycle ready: " + runtime.description()
|
||||
+ "; terrain provider=" + terrain.getClass().getName());
|
||||
@@ -44,18 +72,40 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload");
|
||||
if (arguments.length == 0) {
|
||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
switch (arguments[0]) {
|
||||
case "treasure-map-create" -> createTreasureMapWorld(sender);
|
||||
case "treasure-map-limit" -> runTreasureRegion(sender, () -> checkTreasureMapLimit(sender));
|
||||
case "treasure-map-chest" -> {
|
||||
if (arguments.length != 3) {
|
||||
throw new IllegalArgumentException("Use treasure-map-chest <destination> <CHEST|COPPER_CHEST>");
|
||||
}
|
||||
runTreasureRegion(sender, () -> prepareTreasureMapChest(sender, arguments[1], arguments[2]));
|
||||
}
|
||||
case "treasure-map-inspect" -> {
|
||||
if (arguments.length != 2) {
|
||||
throw new IllegalArgumentException("Use treasure-map-inspect <destination>");
|
||||
}
|
||||
runTreasureRegion(sender, () -> inspectTreasureMapChest(sender, arguments[1]));
|
||||
}
|
||||
case "treasure-map-check" -> runTreasureRegion(sender, () -> verifyTreasureMapLoot(sender));
|
||||
case "treasure-extra-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verify(sender));
|
||||
case "treasure-region-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyRegion(sender));
|
||||
case "treasure-reload-before" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, false));
|
||||
case "treasure-reload-after" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, true));
|
||||
case "treasure-resources" -> treasureLootChecks.reportResourceReloads(sender);
|
||||
case "create" -> create(sender);
|
||||
case "unload" -> unload(sender);
|
||||
case "structures" -> structures(sender);
|
||||
case "terrain-create" -> createTerrain(sender);
|
||||
case "terrain-check" -> checkTerrain(sender);
|
||||
case "terrain-unload" -> unloadTerrain(sender);
|
||||
case "lifecycle-status" -> lifecycleStatus(sender);
|
||||
case "lifecycle-entry" -> lifecycleEntry(sender);
|
||||
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
||||
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
||||
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
||||
@@ -66,6 +116,232 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createTreasureMapWorld(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalStateException("Treasure map fixture requires a player");
|
||||
}
|
||||
runTreasureRegion(sender, () -> prepareTreasureMapWorld(player));
|
||||
}
|
||||
|
||||
private void runTreasureRegion(CommandSender sender, Runnable task) {
|
||||
ensureTreasureMapWorld()
|
||||
.thenCompose(ignored -> J.runRegionFuture(Bukkit.getWorld(TREASURE_KEY), 0, 0, task))
|
||||
.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<?> ensureTreasureMapWorld() {
|
||||
if (Bukkit.getWorld(TREASURE_KEY) != null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
if (WorldCreatorCompat.persistentLevelRoot(TREASURE_KEY).exists()) {
|
||||
return Iris.instance.worldReconciler().loadWorld(ServerProperties.BUKKIT_YML, TREASURE_KEY.toString());
|
||||
}
|
||||
return runAsync(() -> {
|
||||
try {
|
||||
new IrisCreator().name(TREASURE_KEY.toString()).dimension("treasure-map-fixture")
|
||||
.seed(WORLD_SEED).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Treasure map fixture creation failed", failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareTreasureMapWorld(Player player) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED
|
||||
|| !"treasure-map-fixture".equals(IrisToolbelt.access(world).getEngine().getDimension().getLoadKey())) {
|
||||
throw new IllegalStateException("Treasure map fixture world identity is invalid");
|
||||
}
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
world.getBlockAt(x, 160, z).setType(Material.STONE, false);
|
||||
for (int y = 161; y <= 164; y++) {
|
||||
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 0.5, 161, 0.5)).whenComplete((teleported, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
} else if (!Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, new IllegalStateException("Treasure map fixture teleport failed"));
|
||||
} else {
|
||||
player.sendMessage("TREASURE_MAP_READY world=" + TREASURE_KEY
|
||||
+ " version=" + Bukkit.getMinecraftVersion() + " folia=" + J.isFolia());
|
||||
}
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule treasure map fixture teleport");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTreasureMapLimit(CommandSender sender) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||
}
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
IrisStructureLocator.LocateResult located = IrisStructureLocator.locateInPlacementRings(
|
||||
engine, "minecraft:buried_treasure", 2, 2, 50, (x, z) -> true);
|
||||
if (located.status() != IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
throw new IllegalStateException("Blocked treasure placement returned " + located.status());
|
||||
}
|
||||
sender.sendMessage("TREASURE_MAP_LIMIT status=" + located.status());
|
||||
}
|
||||
|
||||
private void prepareTreasureMapChest(CommandSender sender, String destination, String material) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
treasureMapChest(world, destination, material);
|
||||
sender.sendMessage("TREASURE_MAP_CHEST destination=" + destination + " material=" + material);
|
||||
}
|
||||
|
||||
private Container treasureMapChest(World world, String destination, String material) {
|
||||
requireTreasureDestination(destination);
|
||||
if (!List.of("CHEST", "COPPER_CHEST").contains(material)) {
|
||||
throw new IllegalArgumentException("Unknown treasure map container: " + material);
|
||||
}
|
||||
LootTable loot = Bukkit.getLootTable(new NamespacedKey("iris_qa", destination));
|
||||
if (loot == null) {
|
||||
throw new IllegalStateException("Treasure map loot table is absent: " + destination);
|
||||
}
|
||||
world.getBlockAt(2, 161, 2).setType(Material.AIR, false);
|
||||
world.getBlockAt(2, 161, 2).setType(Material.valueOf(material), false);
|
||||
Container chest = (Container) world.getBlockAt(2, 161, 2).getState();
|
||||
((Lootable) chest).setLootTable(loot);
|
||||
((Lootable) chest).setSeed(WORLD_SEED);
|
||||
if (!chest.update(true, false)) {
|
||||
throw new IllegalStateException("Treasure map loot chest was not installed");
|
||||
}
|
||||
return (Container) world.getBlockAt(2, 161, 2).getState();
|
||||
}
|
||||
|
||||
private void verifyTreasureMapLoot(CommandSender sender) {
|
||||
checkTreasureMapLimit(sender);
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
for (String destination : List.of("blocked", "disabled", "found", "mixed")) {
|
||||
Inventory inventory = treasureMapChest(world, destination, "CHEST").getInventory();
|
||||
inspectTreasureMapChest(sender, destination);
|
||||
inventory.remove(Material.DIAMOND);
|
||||
Inventory reopened = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||
if (reopened.contains(Material.DIAMOND)
|
||||
|| !(reopened.contains(Material.FILLED_MAP) || reopened.contains(Material.MAP))) {
|
||||
throw new IllegalStateException("Reopening changed " + destination + " chest loot");
|
||||
}
|
||||
}
|
||||
sender.sendMessage("TREASURE_MAP_CHECK destinations=4 retained=true");
|
||||
}
|
||||
|
||||
private void inspectTreasureMapChest(CommandSender sender, String destination) {
|
||||
requireTreasureDestination(destination);
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
Inventory inventory = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||
ItemStack map = null;
|
||||
int diamonds = 0;
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if ((item.getType() == Material.MAP || item.getType() == Material.FILLED_MAP)
|
||||
&& item.getAmount() == 1 && map == null) {
|
||||
map = item;
|
||||
} else if (item.getType() == Material.DIAMOND) {
|
||||
diamonds += item.getAmount();
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected chest loot: " + item);
|
||||
}
|
||||
}
|
||||
if (map == null || diamonds != 1) {
|
||||
throw new IllegalStateException("Invalid " + destination + " chest: map=" + map + " diamonds=" + diamonds);
|
||||
}
|
||||
boolean expectedLocated = destination.equals("found") || destination.equals("mixed");
|
||||
MapView view = map.getItemMeta() instanceof MapMeta meta && meta.hasMapView() ? meta.getMapView() : null;
|
||||
if ((view != null) != expectedLocated) {
|
||||
throw new IllegalStateException("Unexpected map data for " + destination + ": " + map);
|
||||
}
|
||||
String result = "TREASURE_MAP_INSPECT destination=" + destination + " diamonds=1 located=" + expectedLocated;
|
||||
if (view != null) {
|
||||
StructureSearchResult target = world.locateNearestStructure(
|
||||
new Location(world, 2, 161, 2), Structure.DESERT_PYRAMID, 50, false);
|
||||
if (target == null || target.getStructure() != Structure.DESERT_PYRAMID) {
|
||||
throw new IllegalStateException("Map has no verified pyramid target");
|
||||
}
|
||||
int targetX = target.getLocation().getBlockX();
|
||||
int targetZ = target.getLocation().getBlockZ();
|
||||
if (view.getWorld() != world || view.getScale() != MapView.Scale.CLOSEST
|
||||
|| view.getCenterX() != Math.floorDiv(targetX + 64, 128) * 128
|
||||
|| view.getCenterZ() != Math.floorDiv(targetZ + 64, 128) * 128) {
|
||||
throw new IllegalStateException("Map data does not cover the verified pyramid at " + target.getLocation());
|
||||
}
|
||||
result += " mapId=" + view.getId() + " targetX=" + targetX + " targetZ=" + targetZ
|
||||
+ " centerX=" + view.getCenterX() + " centerZ=" + view.getCenterZ();
|
||||
}
|
||||
sender.sendMessage(result);
|
||||
}
|
||||
|
||||
private void requireTreasureDestination(String destination) {
|
||||
if (!List.of("blocked", "disabled", "found", "mixed").contains(destination)) {
|
||||
throw new IllegalArgumentException("Unknown treasure map destination: " + destination);
|
||||
}
|
||||
}
|
||||
|
||||
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||
Path registry = levelRoot.resolve("iris/worlds.json");
|
||||
boolean registered = Files.exists(registry)
|
||||
&& Files.readString(registry).contains("\"" + COMMAND_KEY + "\"");
|
||||
String startupName = IrisWorldStorage.configuredWorldName(COMMAND_KEY, levelRoot.getFileName().toString());
|
||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
boolean configured = configuration.getConfigurationSection("worlds") != null
|
||||
&& configuration.getConfigurationSection("worlds").contains(startupName);
|
||||
File quarantineRoot = levelRoot.resolve("dimensions/iris").toFile();
|
||||
File[] quarantines = quarantineRoot.listFiles((directory, name) -> name.startsWith(".iris-delete-"));
|
||||
int quarantineCount = quarantines == null ? 0 : quarantines.length;
|
||||
Path queueFile = Bukkit.getPluginManager().getPlugin("Iris").getDataFolder().toPath()
|
||||
.resolve("pending-world-deletes.txt");
|
||||
List<String> queue = Files.exists(queueFile) ? Files.readAllLines(queueFile) : List.of();
|
||||
boolean queued = quarantineCount > 0;
|
||||
if (quarantines != null) {
|
||||
for (File quarantine : quarantines) {
|
||||
queued &= queue.contains(quarantine.getName());
|
||||
}
|
||||
}
|
||||
boolean disk = Files.exists(storage, LinkOption.NOFOLLOW_LINKS);
|
||||
String cleanup = disk ? "present" : quarantineCount == 0 ? "deleted" : queued ? "queued" : "unsafe";
|
||||
boolean idle = true;
|
||||
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||
for (LifecycleOperationCoordinator.Domain domain : LifecycleOperationCoordinator.Domain.values()) {
|
||||
idle &= coordinator.active(domain).isEmpty();
|
||||
}
|
||||
sender.sendMessage("IRIS_LIFECYCLE loaded=" + (world != null) + " registered=" + registered
|
||||
+ " configured=" + configured + " disk=" + disk + " quarantines=" + quarantineCount
|
||||
+ " cleanup=" + cleanup + " idle=" + idle);
|
||||
}
|
||||
|
||||
private void lifecycleEntry(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalArgumentException("Lifecycle entry requires a player");
|
||||
}
|
||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||
if (world == null || player.getWorld() != world || world.getSeed() != WORLD_SEED
|
||||
|| !IrisToolbelt.isIrisWorld(world)) {
|
||||
throw new IllegalStateException("Player did not enter the requested seeded Iris world");
|
||||
}
|
||||
Location feet = player.getLocation();
|
||||
Material floor = world.getBlockAt(feet.getBlockX(), feet.getBlockY() - 1, feet.getBlockZ()).getType();
|
||||
if (floor != Material.LIME_WOOL || !floor.isSolid()
|
||||
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY(), feet.getBlockZ()).isPassable()
|
||||
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY() + 1, feet.getBlockZ()).isPassable()) {
|
||||
throw new IllegalStateException("Lifecycle player entry lacks a safe fixture floor");
|
||||
}
|
||||
sender.sendMessage("IRIS_LIFECYCLE_ENTRY world=" + COMMAND_KEY + " seed=" + world.getSeed()
|
||||
+ " floor=lime_wool safe=true");
|
||||
}
|
||||
|
||||
private void create(CommandSender sender) throws ReflectiveOperationException {
|
||||
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
||||
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
||||
@@ -124,6 +400,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
int y = world.getHighestBlockYAt(x, z);
|
||||
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
||||
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
||||
|| world.getBlockAt(x, world.getMinHeight(), z).getType() != Material.GREEN_CONCRETE
|
||||
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
||||
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
||||
+ ": " + world.getBlockAt(x, y, z).getType());
|
||||
@@ -212,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
}
|
||||
|
||||
public boolean regionized() {
|
||||
return false;
|
||||
return J.isFolia();
|
||||
}
|
||||
|
||||
public boolean primaryThread() {
|
||||
return Bukkit.isPrimaryThread();
|
||||
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> runGlobal(Runnable task) {
|
||||
@@ -229,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
result.completeExceptionally(failure);
|
||||
}
|
||||
};
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
if (primaryThread()) {
|
||||
wrapped.run();
|
||||
} else {
|
||||
Bukkit.getScheduler().runTask(this, wrapped);
|
||||
} else if (!J.runGlobal(wrapped)) {
|
||||
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.world.loot.IrisVanillaLootTable;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import io.papermc.paper.event.world.StructuresLocateEvent;
|
||||
import io.papermc.paper.event.server.ServerResourcesReloadedEvent;
|
||||
import io.papermc.paper.math.Position;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.world.LootGenerateEvent;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class TreasureLootChecks implements Listener {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||
private static final List<String> CHESTS = List.of(
|
||||
"CHEST", "COPPER_CHEST", "EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST",
|
||||
"OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST",
|
||||
"WAXED_WEATHERED_COPPER_CHEST", "WAXED_OXIDIZED_COPPER_CHEST");
|
||||
private final IrisVanillaLootTable reloadTable = new IrisVanillaLootTable("iris_qa:reload");
|
||||
private final AtomicInteger resourceReloads = new AtomicInteger();
|
||||
private LocateAction locateAction = LocateAction.PASS;
|
||||
private int locateEvents;
|
||||
private int lootEvents;
|
||||
private boolean lootEventsOwned = true;
|
||||
|
||||
public TreasureLootChecks(JavaPlugin plugin) {
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
public void verify(CommandSender sender) {
|
||||
World world = requireWorld();
|
||||
verifyCopperLoot(world);
|
||||
sender.sendMessage("TREASURE_COPPER_LOOT variants=8 normal=true diamonds=1");
|
||||
verifyInventoryRollback(world);
|
||||
sender.sendMessage("TREASURE_LOOT_ROLLBACK contents=true nativeTable=true seed=true retry=true");
|
||||
verifyLocateEvents(world);
|
||||
sender.sendMessage("TREASURE_LOCATE_EVENTS cancelled=true replaced=true calls=2");
|
||||
sender.sendMessage("TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true");
|
||||
}
|
||||
|
||||
public void verifyRegion(CommandSender sender) {
|
||||
World world = requireWorld();
|
||||
if (!J.isOwnedByCurrentRegion(world, 0, 0)) {
|
||||
throw new IllegalStateException("Fixture does not own its loot region");
|
||||
}
|
||||
lootEvents = 0;
|
||||
lootEventsOwned = true;
|
||||
verifyCopperLoot(world);
|
||||
if (lootEvents != CHESTS.size() || !lootEventsOwned) {
|
||||
throw new IllegalStateException("Loot event ownership failed: count=" + lootEvents
|
||||
+ " owned=" + lootEventsOwned);
|
||||
}
|
||||
sender.sendMessage("TREASURE_REGION_LOOT copper=8 normal=1 events=" + lootEvents
|
||||
+ " owned=true folia=" + J.isFolia());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLoot(LootGenerateEvent event) {
|
||||
if (event.getWorld().getKey().equals(WORLD_KEY) && event.isPlugin()) {
|
||||
Location origin = event.getLootContext().getLocation();
|
||||
lootEvents++;
|
||||
lootEventsOwned &= J.isOwnedByCurrentRegion(event.getWorld(),
|
||||
origin.getBlockX() >> 4, origin.getBlockZ() >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyReload(CommandSender sender, boolean reloaded) {
|
||||
World world = requireWorld();
|
||||
Material expected = reloaded ? Material.EMERALD : Material.DIAMOND;
|
||||
Collection<ItemStack> items = reloadTable.getLoot(
|
||||
false, 783L, InventorySlotType.STORAGE, world, 2, 161, 2);
|
||||
if (items.size() != 1 || items.iterator().next().getType() != expected) {
|
||||
throw new IllegalStateException("Reloaded loot did not contain " + expected + ": " + items);
|
||||
}
|
||||
sender.sendMessage("TREASURE_RELOAD material=" + expected);
|
||||
}
|
||||
|
||||
public void reportResourceReloads(CommandSender sender) {
|
||||
sender.sendMessage("TREASURE_RESOURCES revision=" + resourceReloads.get());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onResourcesReloaded(ServerResourcesReloadedEvent event) {
|
||||
resourceReloads.incrementAndGet();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLocate(StructuresLocateEvent event) {
|
||||
if (!event.getWorld().getKey().equals(WORLD_KEY) || locateAction == LocateAction.PASS) {
|
||||
return;
|
||||
}
|
||||
locateEvents++;
|
||||
if (locateAction == LocateAction.CANCEL) {
|
||||
event.setCancelled(true);
|
||||
} else {
|
||||
event.setResult(new StructuresLocateEvent.Result(Position.block(123, 100, 456), Structure.DESERT_PYRAMID));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCopperLoot(World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Block block = world.getBlockAt(4, 161, 4);
|
||||
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(block.getChunk());
|
||||
try {
|
||||
for (String name : CHESTS) {
|
||||
block.setType(Material.AIR, false);
|
||||
block.setType(Material.valueOf(name), false);
|
||||
if (!BukkitBlockResolution.isStorageChest(block.getBlockData())) {
|
||||
throw new IllegalStateException("Chest is excluded from Iris loot: " + name);
|
||||
}
|
||||
EngineBukkitOps.update(engine, 4, 161, 4, block.getChunk(), mantle);
|
||||
Container chest = (Container) block.getState();
|
||||
int diamonds = 0;
|
||||
for (ItemStack item : chest.getInventory().getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() != Material.DIAMOND) {
|
||||
throw new IllegalStateException("Unexpected " + name + " loot: " + item);
|
||||
}
|
||||
diamonds += item.getAmount();
|
||||
}
|
||||
if (diamonds != 1) {
|
||||
throw new IllegalStateException(name + " contains " + diamonds + " diamonds");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mantle.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyInventoryRollback(World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Block block = world.getBlockAt(6, 161, 4);
|
||||
block.setType(Material.AIR, false);
|
||||
block.setType(Material.CHEST, false);
|
||||
Chest chest = (Chest) block.getState();
|
||||
chest.getBlockInventory().setItem(0, new ItemStack(Material.EMERALD, 3));
|
||||
chest = (Chest) block.getState();
|
||||
NamespacedKey key = new NamespacedKey("iris_qa", "reload");
|
||||
chest.setLootTable(Bukkit.getLootTable(key));
|
||||
chest.setSeed(913L);
|
||||
if (!chest.update(true, false)) {
|
||||
throw new IllegalStateException("Could not set native loot for rollback check");
|
||||
}
|
||||
Inventory inventory = chest.getBlockInventory();
|
||||
IllegalStateException injected = new IllegalStateException("Fixture insertion interruption");
|
||||
Inventory interrupted = (Inventory) Proxy.newProxyInstance(Inventory.class.getClassLoader(),
|
||||
new Class<?>[]{Inventory.class}, (proxy, method, arguments) -> {
|
||||
Object result;
|
||||
try {
|
||||
result = method.invoke(inventory, arguments);
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw failure.getCause();
|
||||
}
|
||||
if (method.getName().equals("addItem")) {
|
||||
throw injected;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
KList<IrisLootTable> tables = new KList<>();
|
||||
tables.add(new IrisVanillaLootTable("iris_qa:reload"));
|
||||
try {
|
||||
EngineBukkitOps.addItems(engine, false, interrupted, tables, InventorySlotType.STORAGE,
|
||||
world, 6, 161, 4);
|
||||
throw new IllegalStateException("Interrupted insertion did not fail");
|
||||
} catch (IllegalStateException failure) {
|
||||
if (failure != injected) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
Chest restored = (Chest) block.getState();
|
||||
if (restored.getLootTable() == null || !restored.getLootTable().getKey().equals(key)
|
||||
|| restored.getSeed() != 913L) {
|
||||
throw new IllegalStateException("Rollback lost the native loot table or seed");
|
||||
}
|
||||
ItemStack existing = restored.getSnapshotInventory().getItem(0);
|
||||
if (existing == null || existing.getType() != Material.EMERALD || existing.getAmount() != 3) {
|
||||
throw new IllegalStateException("Rollback lost the original contents: " + existing);
|
||||
}
|
||||
EngineBukkitOps.addItems(engine, false, restored.getBlockInventory(), tables, InventorySlotType.STORAGE,
|
||||
world, 6, 161, 4);
|
||||
int diamonds = 0;
|
||||
int emeralds = 0;
|
||||
for (ItemStack item : restored.getBlockInventory().getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() == Material.DIAMOND) {
|
||||
diamonds += item.getAmount();
|
||||
} else if (item.getType() == Material.EMERALD) {
|
||||
emeralds += item.getAmount();
|
||||
}
|
||||
}
|
||||
if (diamonds != 2 || emeralds != 3 || ((Chest) block.getState()).getLootTable() != null) {
|
||||
throw new IllegalStateException("Loot retry failed: diamonds=" + diamonds + " emeralds=" + emeralds);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyLocateEvents(World world) {
|
||||
Location origin = new Location(world, 2, 161, 2);
|
||||
locateEvents = 0;
|
||||
try {
|
||||
locateAction = LocateAction.CANCEL;
|
||||
if (world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false) != null) {
|
||||
throw new IllegalStateException("Cancelled structure search returned a target");
|
||||
}
|
||||
locateAction = LocateAction.REPLACE;
|
||||
StructureSearchResult result = world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false);
|
||||
if (result == null || result.getStructure() != Structure.DESERT_PYRAMID
|
||||
|| result.getLocation().getBlockX() != 123 || result.getLocation().getBlockY() != 100
|
||||
|| result.getLocation().getBlockZ() != 456 || locateEvents != 2) {
|
||||
throw new IllegalStateException("Structure search ignored event result: " + result);
|
||||
}
|
||||
} finally {
|
||||
locateAction = LocateAction.PASS;
|
||||
}
|
||||
}
|
||||
|
||||
private World requireWorld() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
private enum LocateAction {
|
||||
PASS, CANCEL, REPLACE
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,15 @@
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly files(providers.gradleProperty('irisJar').get())
|
||||
compileOnly files(providers.gradleProperty('irisGsonJar').get())
|
||||
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['resources']
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += '-parameters'
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
archiveBaseName = 'IrisNaturalSpawningFixture'
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: IrisNaturalSpawningFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.NaturalSpawningFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris]
|
||||
commands:
|
||||
irisspawnqa:
|
||||
permission: irisspawnqa.run
|
||||
permissions:
|
||||
irisspawnqa.run:
|
||||
default: op
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'iris-natural-spawning-fixture'
|
||||
+701
@@ -0,0 +1,701 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEntityType;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Difficulty;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class NaturalSpawningFixture extends JavaPlugin implements Listener {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "spawn_qa");
|
||||
private static final long WORLD_SEED = 78264193L;
|
||||
private static final int FROG_CAPACITY = 16;
|
||||
private static final int ZOMBIE_CAPACITY = 2;
|
||||
private static final int BIOME_READY_ATTEMPTS = 60;
|
||||
private static final long BIOME_RETRY_TICKS = 10L;
|
||||
|
||||
private boolean previousAmbient;
|
||||
private boolean previousMarkers;
|
||||
private boolean previousForcedPersistence;
|
||||
private volatile boolean ready;
|
||||
private int firstZombieBatch;
|
||||
private int ambientEvents;
|
||||
private int cancelledAmbientEvents;
|
||||
private final ThreadLocal<Boolean> ambientInvocation = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
previousAmbient = IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
|
||||
previousMarkers = IrisSettings.get().getWorld().isMarkerEntitySpawningSystem();
|
||||
previousForcedPersistence = IrisSettings.get().getWorld().isForcePersistEntities();
|
||||
IrisSettings.get().getWorld().setAmbientEntitySpawningSystem(false);
|
||||
IrisSettings.get().getWorld().setMarkerEntitySpawningSystem(false);
|
||||
IrisSettings.get().getWorld().setForcePersistEntities(false);
|
||||
getLogger().info("Natural spawning fixture ready; commands use the Iris ambient path on the owning region.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
IrisSettings.get().getWorld().setAmbientEntitySpawningSystem(previousAmbient);
|
||||
IrisSettings.get().getWorld().setMarkerEntitySpawningSystem(previousMarkers);
|
||||
IrisSettings.get().getWorld().setForcePersistEntities(previousForcedPersistence);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|seeded|completion|surface|cave");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
switch (arguments[0]) {
|
||||
case "setup" -> setup(sender);
|
||||
case "frogs" -> runRegion(sender, () -> spawnFrogs(sender));
|
||||
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
||||
case "stress" -> runRegion(sender, () -> stress(sender));
|
||||
case "check" -> runRegion(sender, () -> check(sender));
|
||||
case "seeded" -> seeded(sender);
|
||||
case "completion" -> completion(sender);
|
||||
case "surface" -> view(sender, 67D);
|
||||
case "cave" -> view(sender, -46D);
|
||||
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void isolateSpawns(CreatureSpawnEvent event) {
|
||||
if (!WORLD_KEY.equals(event.getLocation().getWorld().getKey())) {
|
||||
return;
|
||||
}
|
||||
if (!ambientInvocation.get()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
ambientEvents++;
|
||||
if (event.isCancelled()) {
|
||||
cancelledAmbientEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
private void setup(CommandSender sender) {
|
||||
if (!(sender instanceof Player)) {
|
||||
throw new IllegalArgumentException("Setup requires a player");
|
||||
}
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world != null) {
|
||||
require(IrisToolbelt.isIrisWorld(world) && world.getSeed() == WORLD_SEED,
|
||||
"Existing fixture key is not the owned Iris world with the expected seed");
|
||||
ready = false;
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> configureWorld(sender, world));
|
||||
return;
|
||||
}
|
||||
ready = false;
|
||||
Bukkit.getAsyncScheduler().runNow(this, task -> createWorld(sender));
|
||||
}
|
||||
|
||||
private void createWorld(CommandSender sender) {
|
||||
try {
|
||||
World world = new IrisCreator().name(WORLD_KEY.toString()).dimension("spawn-qa")
|
||||
.seed(WORLD_SEED).create();
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> configureWorld(sender, world));
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureWorld(CommandSender sender, World world) {
|
||||
try {
|
||||
world.setDifficulty(Difficulty.NORMAL);
|
||||
world.setTime(6000L);
|
||||
world.setStorm(false);
|
||||
world.setThundering(false);
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
chunks.add(retainChunk(world, chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new)).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(sender, failure);
|
||||
} else {
|
||||
Bukkit.getRegionScheduler().execute(this, world, 0, 0, () -> prepareChunk(sender, world));
|
||||
}
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> retainChunk(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> retained = new CompletableFuture<>();
|
||||
world.getChunkAtAsync(chunkX, chunkZ, true).whenComplete((chunk, failure) -> {
|
||||
if (failure != null) {
|
||||
retained.completeExceptionally(failure);
|
||||
return;
|
||||
}
|
||||
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
chunk.addPluginChunkTicket(this);
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
retained.complete(null);
|
||||
} catch (Throwable error) {
|
||||
retained.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
return retained;
|
||||
}
|
||||
|
||||
private void prepareChunk(CommandSender sender, World world) {
|
||||
try {
|
||||
require(WORLD_KEY.equals(world.getKey()) && IrisToolbelt.isIrisWorld(world)
|
||||
&& world.getSeed() == WORLD_SEED, "Fixture world ownership changed");
|
||||
Chunk chunk = world.getChunkAt(0, 0);
|
||||
int removed = 0;
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
entity.remove();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
require(!(entity instanceof LivingEntity) || entity instanceof Player || !entity.isValid(),
|
||||
"Fixture target chunk retained a living mob after cleanup: " + entity.getType());
|
||||
}
|
||||
getLogger().info("SPAWN_QA_CLEANUP removed=" + removed + " chunk=0,0");
|
||||
firstZombieBatch = 0;
|
||||
ambientEvents = 0;
|
||||
cancelledAmbientEvents = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
chunk.getBlock(x, 64, z).setType(Material.GRASS_BLOCK, false);
|
||||
for (int y = 65; y <= 68; y++) {
|
||||
chunk.getBlock(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
for (int y = -48; y <= -42; y++) {
|
||||
boolean shell = y == -48 || y == -42 || x == 0 || x == 15 || z == 0 || z == 15;
|
||||
chunk.getBlock(x, y, z).setType(shell ? Material.STONE : Material.CAVE_AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED) {
|
||||
throw new IllegalStateException("Fixture is not the requested Iris world");
|
||||
}
|
||||
awaitBiomeEnvironment(sender, world, 0);
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitBiomeEnvironment(CommandSender sender, World world, int attempt) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
engine.getSurfaceBiomeEnvironment(8, 8);
|
||||
ready = true;
|
||||
teleport(sender, world, 67D, "SPAWN_QA_SETUP world=" + WORLD_KEY + " seed=" + WORLD_SEED);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading() || attempt >= BIOME_READY_ATTEMPTS) {
|
||||
fail(sender, unavailable);
|
||||
return;
|
||||
}
|
||||
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0,
|
||||
task -> awaitBiomeEnvironment(sender, world, attempt + 1), BIOME_RETRY_TICKS);
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void completion(CommandSender sender) {
|
||||
World world = requireWorld();
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, false);
|
||||
runRegion(sender, () -> beginCompletion(sender, world));
|
||||
});
|
||||
}
|
||||
|
||||
private void beginCompletion(CommandSender sender, World world) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Object manager = engine.getWorldManager();
|
||||
Field field = manager.getClass().getDeclaredField("chunkMaintenance");
|
||||
field.setAccessible(true);
|
||||
Object maintenance = field.get(manager);
|
||||
Method run = maintenance.getClass().getDeclaredMethod("runInitialSpawn", World.class, int.class, int.class, Runnable.class);
|
||||
run.setAccessible(true);
|
||||
Field pending = maintenance.getClass().getDeclaredField("markerFlagQueue");
|
||||
pending.setAccessible(true);
|
||||
AtomicInteger callbacks = new AtomicInteger();
|
||||
Runnable callback = () -> {
|
||||
require(Bukkit.isOwnedByCurrentRegion(world, 0, 0), "Initial callback ran outside its owning region");
|
||||
callbacks.incrementAndGet();
|
||||
Chunk chunk = world.getChunkAt(0, 0);
|
||||
invokeAmbient(runtimeSpawn(chunk, "seeded"), chunk, true);
|
||||
};
|
||||
run.invoke(maintenance, world, 0, 0, callback);
|
||||
awaitSkippedInitial(sender, world, new CompletionRun(engine, maintenance, run, pending, callback, callbacks), 0);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not start initial completion test", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitSkippedInitial(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||
try {
|
||||
require(state.callbacks().get() == 0, "Disabled mob spawning consumed the initial pass");
|
||||
if (!((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||
require(attempt < 100, "Skipped initial pass did not release its pending state");
|
||||
awaitSkippedInitial(sender, world, state, attempt + 1);
|
||||
return;
|
||||
}
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||
runRegion(sender, () -> retryInitialCompletion(sender, world, state));
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}, 2L);
|
||||
}
|
||||
|
||||
private void retryInitialCompletion(CommandSender sender, World world, CompletionRun state) {
|
||||
try {
|
||||
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||
awaitInitialCompletion(sender, world, state, 0);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not retry initial spawning", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitInitialCompletion(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||
try {
|
||||
if (state.callbacks().get() == 0 || !((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||
require(attempt < 100, "Initial spawning did not complete after retry");
|
||||
awaitInitialCompletion(sender, world, state, attempt + 1);
|
||||
return;
|
||||
}
|
||||
require(state.callbacks().get() == 1, "Concurrent initial requests spawned duplicate populations");
|
||||
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||
Bukkit.getAsyncScheduler().runNow(this, ignored -> {
|
||||
try {
|
||||
require(state.engine().getMantle().getMantle().hasFlag(0, 0, MantleFlag.INITIAL_SPAWNED_MARKER), "Initial completion was not persisted");
|
||||
require(state.callbacks().get() == 1, "Completed initial pass ran again");
|
||||
evidence(sender, "SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true");
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}, 2L);
|
||||
}
|
||||
|
||||
private void seeded(CommandSender sender) {
|
||||
requireWorld();
|
||||
List<List<String>> baseline = new ArrayList<>();
|
||||
sampleInitial(0, false).thenCompose(first -> {
|
||||
baseline.add(first);
|
||||
return sampleInitial(1, false);
|
||||
}).thenCompose(second -> {
|
||||
baseline.add(second);
|
||||
return sampleInitial(1, false);
|
||||
}).thenCompose(repeated -> {
|
||||
require(baseline.get(1).equals(repeated), "Second chunk changed after reversed generation order");
|
||||
return sampleInitial(0, false);
|
||||
}).thenCompose(repeated -> {
|
||||
require(baseline.get(0).equals(repeated), "First chunk changed after reversed generation order");
|
||||
return sampleInitial(0, true);
|
||||
}).thenCompose(marker -> {
|
||||
baseline.add(marker);
|
||||
return sampleInitial(1, true);
|
||||
}).thenCompose(marker -> {
|
||||
baseline.add(marker);
|
||||
return sampleInitial(1, true);
|
||||
}).thenCompose(repeated -> {
|
||||
require(baseline.get(3).equals(repeated), "Second marker changed after reversed generation order");
|
||||
return sampleInitial(0, true);
|
||||
}).whenComplete((repeated, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(sender, failure);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
require(baseline.get(2).equals(repeated), "First marker changed after reversed generation order");
|
||||
evidence(sender, "SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true");
|
||||
} catch (Throwable error) {
|
||||
fail(sender, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<List<String>> sampleInitial(int chunkX, boolean marker) {
|
||||
CompletableFuture<List<String>> result = new CompletableFuture<>();
|
||||
World world = requireWorld();
|
||||
Bukkit.getRegionScheduler().execute(this, world, chunkX, 0, () -> {
|
||||
try {
|
||||
Chunk chunk = world.getChunkAt(chunkX, 0);
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
chunk.getBlock(x, 64, z).setType(Material.GRASS_BLOCK, false);
|
||||
for (int y = 65; y <= 68; y++) {
|
||||
chunk.getBlock(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "seeded");
|
||||
IrisSpawner definition = spawn.environment().data().getSpawnerLoader().load("qa/seeded");
|
||||
require(!definition.canSpawn(spawn.engine(), chunkX, 0), "Ongoing weather/time gate unexpectedly allowed spawning");
|
||||
for (int i = 0; i < 100 + chunkX; i++) {
|
||||
RNG.r.nextLong();
|
||||
}
|
||||
ambientInvocation.set(true);
|
||||
try {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
LivingEntity witness = (LivingEntity) world.spawnEntity(new Location(world, (chunkX << 4) + 8.5, 65, 8.5), EntityType.ZOMBIE);
|
||||
witness.setCustomName("Population witness");
|
||||
witness.setAI(false);
|
||||
witness.setGravity(false);
|
||||
}
|
||||
} finally {
|
||||
ambientInvocation.remove();
|
||||
}
|
||||
if (marker) {
|
||||
invokeInitialMarker(spawn, definition, new IrisPosition((chunkX << 4) + 5, 64, 5));
|
||||
} else {
|
||||
invokeAmbient(spawn, chunk, true);
|
||||
}
|
||||
List<String> snapshot = new ArrayList<>();
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
String name = entity.getCustomName();
|
||||
if (!(entity instanceof LivingEntity living) || name == null || !name.startsWith("Seeded ")) {
|
||||
continue;
|
||||
}
|
||||
ItemStack hand = living.getEquipment().getItemInMainHand();
|
||||
ItemStack helmet = living.getEquipment().getHelmet();
|
||||
require(hand.hasItemMeta() && hand.getItemMeta().hasAttributeModifiers(), "Spawned gear has no authored attributes");
|
||||
require(helmet != null && helmet.getType() == Material.IRON_HELMET, "Spawned helmet was not authored equipment");
|
||||
Location at = entity.getLocation();
|
||||
snapshot.add(entity.getType() + ":" + at.getX() + ":" + at.getY() + ":" + at.getZ()
|
||||
+ ":" + HexFormat.of().formatHex(hand.serializeAsBytes())
|
||||
+ ":" + HexFormat.of().formatHex(helmet.serializeAsBytes()));
|
||||
}
|
||||
require(snapshot.size() >= 2 && snapshot.size() <= 4, "Initial population was blocked by runtime gates or cap: " + snapshot.size());
|
||||
Collections.sort(snapshot);
|
||||
result.complete(List.copyOf(snapshot));
|
||||
} catch (Throwable failure) {
|
||||
result.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private void invokeInitialMarker(RuntimeSpawn spawn, IrisSpawner definition, IrisPosition position) {
|
||||
ambientInvocation.set(true);
|
||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||
Method method = spawn.spawner().getClass().getDeclaredMethod("spawnMarker", IrisPosition.class, IrisSpawner.class, IrisMarker.class, RNG.class, boolean.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(spawn.spawner(), position, definition, null, EntitySpawnSeed.marker(spawn.engine().getSeedManager().getEntity(), position.getX(), position.getY(), position.getZ()), true);
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw new IllegalStateException("Initial marker spawning failed", failure.getCause());
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not invoke initial marker spawning", failure);
|
||||
} finally {
|
||||
ambientInvocation.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnFrogs(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||
for (int attempt = 0; attempt < 12; attempt++) {
|
||||
invokeAmbient(spawn, chunk, false);
|
||||
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
||||
}
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
||||
"Frog ambient category did not fill: " + spawnDiagnostics(spawn, chunk));
|
||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Unexpected zombie before cave phase");
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_FROGS frogs=" + count(chunk, EntityType.FROG)
|
||||
+ " persistent=true removable=true ambient=true");
|
||||
}
|
||||
|
||||
private void spawnZombies(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Frogs must fill their category before cave spawning");
|
||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||
invokeAmbient(spawn, chunk, false);
|
||||
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
||||
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
||||
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Cave spawning changed the passive population");
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_ZOMBIES frogs=" + count(chunk, EntityType.FROG)
|
||||
+ " zombies=" + firstZombieBatch + " firstBatch=" + firstZombieBatch + " ambient=true");
|
||||
}
|
||||
|
||||
private void stress(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||
for (int attempt = 0; attempt < 50; attempt++) {
|
||||
invokeAmbient(spawn, chunk, false);
|
||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
||||
}
|
||||
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
||||
}
|
||||
|
||||
private void check(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Passive population differs");
|
||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Hostile population differs");
|
||||
require(firstZombieBatch == ZOMBIE_CAPACITY, "First hostile batch was not pinned");
|
||||
require(chunk.getBlock(8, -47, 8).getType() == Material.CAVE_AIR, "Cave is not real CAVE_AIR");
|
||||
require(chunk.getBlock(8, -47, 8).getLightLevel() == 0, "Cave is not dark");
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == EntityType.ZOMBIE) {
|
||||
require(entity.getLocation().getY() < -42, "Hostile spawned outside the cave");
|
||||
}
|
||||
}
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_RESULT frogs=16 zombies=2 firstBatch=2 persistent=true removable=true caveAir=true light=0");
|
||||
}
|
||||
|
||||
private RuntimeSpawn runtimeSpawn(Chunk chunk, String biomeKey) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(chunk.getWorld()).getEngine();
|
||||
BiomeEnvironment surface = engine.getSurfaceBiomeEnvironment(8, 8);
|
||||
IrisBiome biome = surface.data().getBiomeLoader().load(biomeKey);
|
||||
require(biome != null, "Synthetic biome was not loaded");
|
||||
BiomeEnvironment selected = new BiomeEnvironment(surface.activationId(), biome, surface.region(),
|
||||
surface.dimension(), surface.data());
|
||||
Object manager = engine.getWorldManager();
|
||||
Field field = manager.getClass().getDeclaredField("entitySpawner");
|
||||
field.setAccessible(true);
|
||||
Object spawner = field.get(manager);
|
||||
Method method = spawner.getClass().getDeclaredMethod("spawnAmbient", Chunk.class, boolean.class, BiomeEnvironment.class);
|
||||
method.setAccessible(true);
|
||||
return new RuntimeSpawn(engine, spawner, method, selected);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not access Iris ambient spawning", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private String spawnDiagnostics(RuntimeSpawn spawn, Chunk chunk) {
|
||||
World world = chunk.getWorld();
|
||||
int floor = world.getHighestBlockYAt(8, 8, HeightMap.OCEAN_FLOOR);
|
||||
int top = world.getHighestBlockYAt(8, 8, HeightMap.WORLD_SURFACE);
|
||||
Map<String, Integer> categories = new TreeMap<>();
|
||||
Map<String, Integer> types = new TreeMap<>();
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
categories.merge(BukkitEntityType.of(entity.getType()).spawnCategory(), 1, Integer::sum);
|
||||
types.merge(entity.getType().name(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
StringBuilder diagnostics = new StringBuilder("SPAWN_QA_DIAGNOSTICS frogs=")
|
||||
.append(count(chunk, EntityType.FROG)).append(" zombies=").append(count(chunk, EntityType.ZOMBIE))
|
||||
.append(" categories=").append(categories).append(" types=").append(types)
|
||||
.append(" loadLevel=").append(chunk.getLoadLevel())
|
||||
.append(" spawnMobs=").append(world.getGameRuleValue(GameRules.SPAWN_MOBS))
|
||||
.append(" animals=").append(world.getAllowAnimals()).append(" monsters=").append(world.getAllowMonsters())
|
||||
.append(" floor=").append(floor).append(" top=").append(top)
|
||||
.append(" support=").append(world.getBlockAt(8, top, 8).getType())
|
||||
.append(" above=").append(world.getBlockAt(8, top + 1, 8).getType())
|
||||
.append(" light=").append(world.getBlockAt(8, top + 1, 8).getLightLevel())
|
||||
.append(" events=").append(ambientEvents).append(" cancelled=").append(cancelledAmbientEvents);
|
||||
for (String key : spawn.environment().biome().getEntitySpawners()) {
|
||||
IrisSpawner spawner = spawn.environment().data().getSpawnerLoader().load(key);
|
||||
if (spawner == null) {
|
||||
diagnostics.append(" spawner=").append(key).append(":missing");
|
||||
continue;
|
||||
}
|
||||
diagnostics.append(" spawner=").append(key).append(" canSpawn=")
|
||||
.append(spawner.canSpawn(spawn.engine(), chunk.getX(), chunk.getZ()))
|
||||
.append(" excluded=").append(spawner.isCompatExcluded())
|
||||
.append(" group=").append(spawner.getGroup()).append(" capacity=").append(spawner.getMaxEntitiesPerChunk());
|
||||
for (IrisEntitySpawn entry : spawner.getSpawns()) {
|
||||
IrisEntity entity = entry.getRealEntity(spawn.engine());
|
||||
diagnostics.append(" entity=").append(entry.getEntity());
|
||||
if (entity == null) {
|
||||
diagnostics.append(":missing");
|
||||
continue;
|
||||
}
|
||||
diagnostics.append(" type=").append(entity.getBukkitType()).append(" category=").append(entity.spawnCategory())
|
||||
.append(" surface=").append(entity.getSurface())
|
||||
.append(" supportMatches=").append(entity.getSurface().matches(world.getBlockAt(8, top, 8)))
|
||||
.append(" bounds=").append(BukkitPlatform.entityBoundingBox(entity.getBukkitType()));
|
||||
}
|
||||
}
|
||||
return diagnostics.toString();
|
||||
}
|
||||
|
||||
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk, boolean initial) {
|
||||
ambientInvocation.set(true);
|
||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||
spawn.method().invoke(spawn.spawner(), chunk, initial, spawn.environment());
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not invoke Iris ambient spawning", failure);
|
||||
} finally {
|
||||
ambientInvocation.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private int count(Chunk chunk, EntityType type) {
|
||||
int count = 0;
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == type) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void verifyPersistence(Chunk chunk) {
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == EntityType.FROG || entity.getType() == EntityType.ZOMBIE) {
|
||||
require(entity.isPersistent(), "Ambient entity stopped participating in chunk saves");
|
||||
require(entity instanceof LivingEntity living && living.getRemoveWhenFarAway(),
|
||||
"Ambient entity has distance despawning disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void view(CommandSender sender, double y) {
|
||||
teleport(sender, requireWorld(), y, "SPAWN_QA_VIEW y=" + (int) y);
|
||||
}
|
||||
|
||||
private void teleport(CommandSender sender, World world, double y, String message) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalArgumentException("Viewing requires a player");
|
||||
}
|
||||
player.getScheduler().run(this, task -> {
|
||||
player.setGameMode(GameMode.CREATIVE);
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
player.teleportAsync(new Location(world, 40.5D, y, 8.5D)).whenComplete((success, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(success)) {
|
||||
fail(sender, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
|
||||
} else {
|
||||
notifySender(sender, message);
|
||||
}
|
||||
});
|
||||
}, null);
|
||||
}
|
||||
|
||||
private World requireWorld() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
require(ready && world != null, "Run setup before a spawning phase");
|
||||
return world;
|
||||
}
|
||||
|
||||
private void runRegion(CommandSender sender, Runnable operation) {
|
||||
World world = requireWorld();
|
||||
Bukkit.getRegionScheduler().execute(this, world, 0, 0, () -> {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void evidence(CommandSender sender, String message) {
|
||||
getLogger().info(message);
|
||||
notifySender(sender, message);
|
||||
}
|
||||
|
||||
private void notifySender(CommandSender sender, String message) {
|
||||
if (sender instanceof Player player) {
|
||||
player.getScheduler().run(this, task -> player.sendMessage(message), null);
|
||||
} else {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(CommandSender sender, Throwable failure) {
|
||||
getLogger().log(Level.SEVERE, "Spawning acceptance failed", failure);
|
||||
notifySender(sender, "SPAWN_QA_FAILED " + failure.getMessage());
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private record CompletionRun(Engine engine, Object maintenance, Method run, Field pending, Runnable callback, AtomicInteger callbacks) {
|
||||
}
|
||||
|
||||
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Cave Zombies",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:grass_block"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
],
|
||||
"entitySpawners": [
|
||||
"qa/cave-zombies"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Frogs",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:grass_block"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
],
|
||||
"entitySpawners": [
|
||||
"qa/frogs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Seeded population",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:grass_block"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
],
|
||||
"entitySpawners": [
|
||||
"qa/seeded"
|
||||
]
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Spawn QA",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "frogs",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "minecraft:frog",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "minecraft:husk",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false,
|
||||
"customName": "Seeded husk",
|
||||
"invulnerable": true,
|
||||
"helmet": {
|
||||
"type": "IRON_HELMET",
|
||||
"minDurability": 0.1,
|
||||
"maxDurability": 0.9
|
||||
},
|
||||
"mainHand": {
|
||||
"type": "IRON_SWORD",
|
||||
"minDurability": 0.1,
|
||||
"maxDurability": 0.9,
|
||||
"attributes": [
|
||||
{
|
||||
"name": "seeded_damage",
|
||||
"attribute": "minecraft:attack_damage",
|
||||
"minAmount": 1.25,
|
||||
"maxAmount": 6.75,
|
||||
"chance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false,
|
||||
"customName": "Seeded zombie",
|
||||
"invulnerable": true,
|
||||
"helmet": {
|
||||
"type": "IRON_HELMET",
|
||||
"minDurability": 0.1,
|
||||
"maxDurability": 0.9
|
||||
},
|
||||
"mainHand": {
|
||||
"type": "IRON_SWORD",
|
||||
"minDurability": 0.1,
|
||||
"maxDurability": 0.9,
|
||||
"attributes": [
|
||||
{
|
||||
"name": "seeded_damage",
|
||||
"attribute": "minecraft:attack_damage",
|
||||
"minAmount": 1.25,
|
||||
"maxAmount": 6.75,
|
||||
"chance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"frogs"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"frogs"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"frogs"
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"group": "CAVE",
|
||||
"maxEntitiesPerChunk": 2,
|
||||
"allowedLightLevels": {
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"spawns": [
|
||||
{
|
||||
"entity": "qa/zombie",
|
||||
"rarity": 1,
|
||||
"minSpawns": 7,
|
||||
"maxSpawns": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"group": "NORMAL",
|
||||
"maxEntitiesPerChunk": 16,
|
||||
"spawns": [
|
||||
{
|
||||
"entity": "qa/frog",
|
||||
"rarity": 1,
|
||||
"minSpawns": 7,
|
||||
"maxSpawns": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"group": "NORMAL",
|
||||
"maxEntitiesPerChunk": 4,
|
||||
"allowedLightLevels": {
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"weather": "DOWNFALL",
|
||||
"timeBlock": {
|
||||
"startHour": 20,
|
||||
"endHour": 3
|
||||
},
|
||||
"maximumRate": {
|
||||
"amount": 1,
|
||||
"per": {
|
||||
"days": 1
|
||||
}
|
||||
},
|
||||
"initialSpawns": [
|
||||
{
|
||||
"entity": "qa/seeded-zombie",
|
||||
"rarity": 1,
|
||||
"minSpawns": 2,
|
||||
"maxSpawns": 4
|
||||
},
|
||||
{
|
||||
"entity": "qa/seeded-husk",
|
||||
"rarity": 2,
|
||||
"minSpawns": 2,
|
||||
"maxSpawns": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export default {
|
||||
name: 'iris-natural-spawning',
|
||||
description: 'Observe category-limited Iris ambient frogs and cave zombies on Folia.',
|
||||
async run(context) {
|
||||
const wait = (predicate, label, timeoutMs = 30000) => context.waitUntil(predicate, { label, timeoutMs, intervalMs: 100 })
|
||||
const command = async (operation, expected, timeoutMs = 30000) => {
|
||||
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
|
||||
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||
return response
|
||||
}
|
||||
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name
|
||||
&& entity.position.x >= 0 && entity.position.x < 16 && entity.position.z >= 0 && entity.position.z < 16)
|
||||
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
||||
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
||||
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
||||
})
|
||||
await context.step('fill the passive category through real Iris ambient spawning', async () => {
|
||||
await command('frogs', 'SPAWN_QA_FROGS frogs=16 persistent=true removable=true ambient=true', 60000)
|
||||
await wait(() => entities('frog').length === 16, 'All sixteen ambient frogs became visible')
|
||||
context.report.irisNaturalSpawning = { visibleFrogs: entities('frog').length }
|
||||
})
|
||||
await context.step('spawn an authored seven-zombie batch with capacity two despite existing frogs', async () => {
|
||||
await command('zombies', 'SPAWN_QA_ZOMBIES frogs=16 zombies=2 firstBatch=2 ambient=true', 60000)
|
||||
await command('cave', 'SPAWN_QA_VIEW y=-46')
|
||||
await wait(() => Math.abs(context.bot.entity.position.y + 46) < 1, 'Underground fixture teleport')
|
||||
await wait(() => context.bot.blockAt(context.bot.entity.position.clone().set(8, -47, 8))?.name === 'cave_air', 'Live cave_air block reached the client')
|
||||
await wait(() => entities('zombie').length === 2, 'Both ambient cave zombies became visible')
|
||||
context.expect(entities('zombie').every(entity => entity.position.y < -42), 'Zombies were not inside the underground chamber')
|
||||
context.report.irisNaturalSpawning.visibleZombies = entities('zombie').length
|
||||
context.report.irisNaturalSpawning.caveAir = true
|
||||
})
|
||||
await context.step('repeat fifty ambient attempts without exceeding the hostile capacity', async () => {
|
||||
await command('stress', 'SPAWN_QA_STRESS attempts=50 zombies=2', 60000)
|
||||
await command('check', 'SPAWN_QA_RESULT frogs=16 zombies=2 firstBatch=2 persistent=true removable=true caveAir=true light=0', 30000)
|
||||
context.expect(entities('zombie').length === 2, 'Client observed a hostile capacity overflow')
|
||||
context.report.irisNaturalSpawning.saturatedAttempts = 50
|
||||
context.report.irisNaturalSpawning.firstHostileBatch = 2
|
||||
context.report.irisNaturalSpawning.removeWhenFarAwayAllowed = true
|
||||
context.report.irisNaturalSpawning.chunkSaving = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 96
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Noise Sources",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"expression": "0.5 + sin(x * 0.15) * 0.25 + cos(y * 0.13) * 0.25"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"expression": "terrain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"seed": 311,
|
||||
"style": {
|
||||
"style": "CLOVER_HERMITE",
|
||||
"zoom": 0.1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-noise',
|
||||
description: 'Verify expression and interpolated noise produce varied, bounded terrain visible to a player.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Noise generation requires an isolated instance')
|
||||
await context.step('create a world with expression and Clover noise', async () => {
|
||||
const response = await context.command('/iris create name=noise_probe type=noise-fixture seed=78264193',
|
||||
/Successfully created your world|Failed to create/, 180000)
|
||||
context.expect(response.includes('Successfully created your world'), response)
|
||||
})
|
||||
await context.step('client observes bounded terrain with varying heights', async () => {
|
||||
let columns = 0
|
||||
let minimum = Infinity
|
||||
let maximum = -Infinity
|
||||
await context.waitUntil(() => {
|
||||
columns = 0
|
||||
minimum = Infinity
|
||||
maximum = -Infinity
|
||||
const center = context.bot.entity.position.floored()
|
||||
for (let x = -8; x <= 8; x++) {
|
||||
for (let z = -8; z <= 8; z++) {
|
||||
let surface = null
|
||||
for (let y = 60; y <= 100; y++) {
|
||||
const position = center.offset(x, y - center.y, z)
|
||||
const block = context.bot.blockAt(position)
|
||||
if (!block) return false
|
||||
if (block.name === 'lime_wool') {
|
||||
if (surface !== null || context.bot.blockAt(position.offset(0, 1, 0))?.name !== 'air') return false
|
||||
surface = y
|
||||
}
|
||||
}
|
||||
if (surface === null || surface < 64 || surface > 96) return false
|
||||
columns++
|
||||
minimum = Math.min(minimum, surface)
|
||||
maximum = Math.max(maximum, surface)
|
||||
}
|
||||
}
|
||||
return columns === 289 && maximum - minimum >= 3
|
||||
}, { label: '289 noise columns with bounded, varied surfaces', timeoutMs: 60000, intervalMs: 250 })
|
||||
context.expect(columns === 289 && maximum - minimum >= 3, 'Client verified noise terrain', { columns, minimum, maximum })
|
||||
context.report.noiseTerrain = { columns, minimum, maximum }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const chunks = [[-64, -64], [64, -64], [-64, 64], [64, 64]]
|
||||
|
||||
export default {
|
||||
name: 'iris-seed-cohesion',
|
||||
description: 'Compare client-visible decorated chunks in two same-seed worlds explored in opposite orders.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Seed cohesion requires an isolated instance')
|
||||
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
|
||||
const currentWorld = async () => {
|
||||
const observation = await context.observe()
|
||||
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||
return observation.worlds.find(entry => entry.id === player?.world)
|
||||
}
|
||||
const enter = async name => {
|
||||
await context.command(`/iris tp ${name}`)
|
||||
await wait(async () => (await currentWorld())?.key === `iris:${name}`
|
||||
|| (await currentWorld())?.name.endsWith(name), `Enter ${name}`)
|
||||
await context.command('/minecraft:gamemode spectator @s')
|
||||
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
|
||||
await context.command('/minecraft:seed', /Seed:.*1337/)
|
||||
}
|
||||
const capture = async coordinates => {
|
||||
const hashes = {}
|
||||
const decorators = {}
|
||||
let decorationCount = 0
|
||||
for (const [cx, cz] of coordinates) {
|
||||
const x = cx * 16
|
||||
const z = cz * 16
|
||||
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
|
||||
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
|
||||
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Chunk sample teleport')
|
||||
const position = context.bot.entity.position.clone()
|
||||
await wait(() => context.bot.blockAt(position.clone().set(x, 1, z))
|
||||
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
|
||||
const digest = createHash('sha256')
|
||||
let decoration = 0
|
||||
let terrain = 0
|
||||
for (let dx = 0; dx < 16; dx++) {
|
||||
for (let dz = 0; dz < 16; dz++) {
|
||||
for (let y = 0; y < 64; y++) {
|
||||
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
|
||||
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
|
||||
digest.update(`${block.stateId},`)
|
||||
if (block.name === 'granite' || block.name === 'andesite') decoration++
|
||||
if (block.name === 'stone') terrain++
|
||||
}
|
||||
}
|
||||
}
|
||||
context.expect(terrain > 1000, 'Fixture terrain was not generated', { cx, cz, terrain })
|
||||
decorationCount += decoration
|
||||
decorators[`${cx},${cz}`] = decoration
|
||||
hashes[`${cx},${cz}`] = digest.digest('hex')
|
||||
}
|
||||
context.expect(decorationCount > 0, 'Fixture decorators were not generated in sampled chunks', { decorators })
|
||||
context.report.decoratorCoverage ??= []
|
||||
context.report.decoratorCoverage.push(decorators)
|
||||
return hashes
|
||||
}
|
||||
const baseline = 'seed-cohesion-forward'
|
||||
const reverse = 'seed-cohesion-reverse'
|
||||
const stateFile = path.join(context.server.directory, '.seed-cohesion.json')
|
||||
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
|
||||
const restart = context.options.command === 'restart-check'
|
||||
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
|
||||
if (restart) {
|
||||
context.expect(jarHash === saved.jarHash, 'Restart-check requires the same Iris jar', { before: saved.jarHash, after: jarHash })
|
||||
context.expect((await context.observe()).processId !== saved.processId, 'Restart-check requires a cold server restart')
|
||||
}
|
||||
const outputs = []
|
||||
for (const [name, coordinates] of [[baseline, chunks], [reverse, [...chunks].reverse()]]) {
|
||||
await context.step(`${restart ? 'Reload' : 'Create'} and sample ${name}`, async () => {
|
||||
if (!restart) {
|
||||
await context.command(`/iris create name=${name} type=cohesion seed=1337`, /Successfully created your world/i, 180000)
|
||||
}
|
||||
await enter(name)
|
||||
const output = await capture(coordinates)
|
||||
if (restart) {
|
||||
for (const [coordinate, hash] of Object.entries(output)) {
|
||||
context.expect(saved.hashes[coordinate] === hash, 'Persisted chunk changed after restart', { name, coordinate, before: saved.hashes[coordinate], after: hash })
|
||||
}
|
||||
Object.assign(output, await capture(coordinates.map(([cx, cz]) => [cx * 3, cz * 3])))
|
||||
}
|
||||
outputs.push(output)
|
||||
})
|
||||
}
|
||||
await context.step('Compare all sampled blocks', async () => {
|
||||
for (const [coordinate, hash] of Object.entries(outputs[0])) {
|
||||
context.expect(outputs[1][coordinate] === hash, 'Same-seed worlds differ',
|
||||
{ coordinate, forward: hash, reverse: outputs[1][coordinate] })
|
||||
}
|
||||
context.report.seedCohesion = { seed: 1337, jarHash, blocksPerChunk: 16384, restart, hashes: outputs[0] }
|
||||
if (!restart) {
|
||||
await writeFile(stateFile, JSON.stringify({ processId: (await context.observe()).processId, jarHash, hashes: outputs[0] }))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
name: 'iris-seeded-entities',
|
||||
description: 'Replay initial chunk and marker populations in reversed order, comparing positions and complete equipment data.',
|
||||
async run(context) {
|
||||
const command = async (operation, expected, timeoutMs) => {
|
||||
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
|
||||
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||
}
|
||||
await context.step('prepare a controlled Iris world', async () => {
|
||||
await command('setup', 'SPAWN_QA_SETUP', 180000)
|
||||
})
|
||||
await context.step('replay initial mobs despite unrelated randomness and live spawn restrictions', async () => {
|
||||
await command('seeded', 'SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true', 60000)
|
||||
await context.waitUntil(() => Object.values(context.bot.entities).some(entity => entity.name === 'zombie' || entity.name === 'husk'), {
|
||||
label: 'Generated mobs reached the client', timeoutMs: 10000, intervalMs: 100
|
||||
})
|
||||
context.report.seededEntities = { chunks: 2, markers: 2, repeats: 2, positions: true, equipmentBytes: true, attributeIds: true }
|
||||
})
|
||||
await context.step('retry a skipped initial pass and persist completion once', async () => {
|
||||
await command('completion', 'SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true', 30000)
|
||||
context.report.seededEntities.completionRetry = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"place": [
|
||||
"object"
|
||||
],
|
||||
"chance": 1,
|
||||
"density": 4,
|
||||
"mode": "CENTER_HEIGHT",
|
||||
"rotation": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Structure Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"structures": [
|
||||
"surface"
|
||||
],
|
||||
"placementId": "surface",
|
||||
"distribution": "DENSITY",
|
||||
"density": 1,
|
||||
"anchor": "SURFACE",
|
||||
"terrain": {
|
||||
"mode": "PRESERVE"
|
||||
},
|
||||
"stilt": {
|
||||
"maxDepth": 16,
|
||||
"palette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:diamond_block"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"structures": [
|
||||
"buried"
|
||||
],
|
||||
"placementId": "buried",
|
||||
"distribution": "DENSITY",
|
||||
"density": 1,
|
||||
"anchor": "HEIGHT_BAND",
|
||||
"minHeight": 24,
|
||||
"maxHeight": 24,
|
||||
"terrain": {
|
||||
"mode": "BORE"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"object": "buried",
|
||||
"connectors": [],
|
||||
"rotatable": false,
|
||||
"rules": {
|
||||
"terminal": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"object": "surface",
|
||||
"connectors": [],
|
||||
"rotatable": false,
|
||||
"rules": {
|
||||
"terminal": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "buried",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "surface",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user