mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Compare commits
297
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3a9449a02 | ||
|
|
1076ab47a7 | ||
|
|
032a8ba46a | ||
|
|
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 |
No files matched your search
@@ -1,85 +1,78 @@
|
|||||||
name: Iris Bug Report
|
name: Bug report
|
||||||
description: File a bug report for Iris
|
description: Something in Iris isn't working.
|
||||||
labels: [ bug ]
|
labels: [bug]
|
||||||
|
type: Bug
|
||||||
|
assignees: [SwiftSwamp]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to fill this out!
|
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).
|
||||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
|
||||||
|
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
|
- type: textarea
|
||||||
id: how
|
id: issue
|
||||||
attributes:
|
attributes:
|
||||||
label: Problem
|
label: What issue are you having?
|
||||||
description: Please give a text description of how you reached the problem
|
description: Include any error message you saw.
|
||||||
value: |
|
|
||||||
1. Install Iris...
|
|
||||||
2. Do this...
|
|
||||||
3. Do that...
|
|
||||||
4. Observe the error...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what
|
id: steps
|
||||||
attributes:
|
attributes:
|
||||||
label: Solution
|
label: Steps to reproduce
|
||||||
description: Explain where you think the problem comes from (optional)
|
placeholder: |
|
||||||
placeholder: The code to place a is missing b and c...
|
1. Start a server with Iris installed
|
||||||
|
2. Change ... in the config
|
||||||
|
3. Run /iris ...
|
||||||
|
4. ... happens instead of ...
|
||||||
validations:
|
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
|
- type: dropdown
|
||||||
id: platform
|
id: platform
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform
|
label: Server software
|
||||||
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
|
|
||||||
options:
|
options:
|
||||||
- Paper
|
- "Paper"
|
||||||
- Purpur
|
- "Purpur"
|
||||||
- Leaf
|
- "Leaf"
|
||||||
- Canvas
|
- "Canvas"
|
||||||
- Folia
|
- "Folia"
|
||||||
- Spigot / CraftBukkit
|
- "Spigot"
|
||||||
- Fabric
|
- "Fabric"
|
||||||
- Forge
|
- "Forge"
|
||||||
- NeoForge
|
- "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'
|
|
||||||
- Other (unsupported)
|
- Other (unsupported)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: loaderversion
|
id: minecraft
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform / Loader Version
|
label: Minecraft 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".
|
options:
|
||||||
placeholder: DO NOT SAY "LATEST"
|
- "26.3"
|
||||||
|
- "26.2"
|
||||||
|
- "26.1.2"
|
||||||
|
- Other (unsupported)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: textarea
|
||||||
id: irisversion
|
id: extra
|
||||||
attributes:
|
attributes:
|
||||||
label: Iris Version
|
label: Anything else?
|
||||||
description: What version of Iris are you running? (see console)
|
description: Other plugins involved, screenshots, or videos.
|
||||||
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."
|
|
||||||
@@ -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
|
name: Suggestion
|
||||||
description: File a feature request for Iris. If you want to report a bug this is not the place.
|
description: Suggest a new feature or a change to Iris.
|
||||||
labels: [ feature ]
|
labels: [enhancement]
|
||||||
|
type: Feature
|
||||||
|
assignees: [SwiftSwamp]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to fill this out!
|
Check the [existing suggestions](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue+label%3Aenhancement) first and add relevant information to the existing issue.
|
||||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
- type: textarea
|
||||||
- type: dropdown
|
id: problem
|
||||||
id: arc
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Adding, Removing, or Changing
|
label: What are your suggestions for new features or changes?
|
||||||
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 ...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: desc
|
id: proposal
|
||||||
attributes:
|
attributes:
|
||||||
label: What are you trying to modify
|
label: How should it work?
|
||||||
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
|
description: Provide as much detail as possible on what you would like changed or added.
|
||||||
value: The way I would implement this is ...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: alternative
|
id: extra
|
||||||
attributes:
|
attributes:
|
||||||
label: Alternatives
|
label: Anything else?
|
||||||
description: What alternatives have you considered?
|
description: Any additional information such as screenshots, examples, etc.
|
||||||
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."
|
|
||||||
@@ -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/
|
.compatwt/
|
||||||
|
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
/VolmLib/
|
||||||
|
/docs/
|
||||||
|
/.perf/
|
||||||
|
/.codegraph/
|
||||||
+452
-296
@@ -1,160 +1,215 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
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.
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
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
|
The licenses for most software and other practical works are designed
|
||||||
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
|
to take away your freedom to share and change the works. By contrast,
|
||||||
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 is intended to guarantee your freedom to
|
||||||
the GNU General Public License for most of our software; it applies also to any other work released this way by its
|
share and change all versions of a program--to make sure it remains free
|
||||||
authors. You can apply it to your programs, too.
|
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
|
When we speak of free software, we are referring to freedom, not
|
||||||
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
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
|
have the freedom to distribute copies of free software (and charge for
|
||||||
that you know you can do these things.
|
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.
|
To protect your rights, we need to prevent others from denying you
|
||||||
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
responsibilities to respect the freedom of others.
|
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
|
For example, if you distribute copies of such a program, whether
|
||||||
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
show them these terms so they know their rights.
|
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:
|
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
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
and/or modify it.
|
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 the developers' and authors' protection, the GPL clearly explains
|
||||||
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
|
that there is no warranty for this free software. For both users' and
|
||||||
will not be attributed erroneously to authors of previous versions.
|
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
|
Some devices are designed to deny users access to install or run
|
||||||
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
|
modified versions of the software inside them, although the manufacturer
|
||||||
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
|
protecting users' freedom to change the software. The systematic
|
||||||
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
domains in future versions of the GPL, as needed to protect the freedom of users.
|
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
|
Finally, every program is threatened constantly by software patents.
|
||||||
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
|
States should not allow patents to restrict development and use of
|
||||||
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
|
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.
|
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
|
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". "
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"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,
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
work "based on" the earlier work.
|
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
|
To "propagate" a work means to do anything with it that, without
|
||||||
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
|
permission, would make you directly or secondarily liable for
|
||||||
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
|
infringement under applicable copyright law, except executing it on a
|
||||||
countries other activities as well.
|
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
|
To "convey" a work means any kind of propagation that enables other
|
||||||
with a user through a computer network, with no transfer of a copy, is not conveying.
|
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"
|
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
|
to the extent that it includes a convenient and prominently visible
|
||||||
notice, and (2)
|
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
|
tells the user that there is no warranty for the work (except to the
|
||||||
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
|
extent that warranties are provided), that licensees may convey the
|
||||||
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
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
|
The "source code" for a work means the preferred form of the work
|
||||||
any non-source form of a 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,
|
A "Standard Interface" means an interface that either is an official
|
||||||
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
|
standard defined by a recognized standards body, or, in the case of
|
||||||
working in that language.
|
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 "System Libraries" of an executable work include anything, other
|
||||||
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
|
packaging a Major Component, but which is not part of that Major
|
||||||
available to the public in source code form. A
|
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
|
"Major Component", in this context, means a major essential component
|
||||||
(kernel, window system, and so on) of the specific operating system
|
(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
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
to run it.
|
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 (
|
The "Corresponding Source" for a work in object code form means all
|
||||||
for an executable work) run the object code and to modify the work, including scripts to control those activities.
|
the source code needed to generate, install, and (for an executable
|
||||||
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
|
work) run the object code and to modify the work, including scripts to
|
||||||
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
|
control those activities. However, it does not include the work's
|
||||||
Source includes interface definition files associated with source files for the work, and the source code for shared
|
System Libraries, or general-purpose tools or generally available free
|
||||||
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
|
programs which are used unmodified in performing those activities but
|
||||||
communication or control flow between those subprograms and other parts of the work.
|
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
|
The Corresponding Source need not include anything that users
|
||||||
Corresponding Source.
|
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
|
All rights granted under this License are granted for the term of
|
||||||
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
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
|
You may make, run and propagate covered works that you do not
|
||||||
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
|
convey, without conditions so long as your license otherwise remains
|
||||||
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
|
in force. You may convey covered works to others for the sole purpose
|
||||||
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
|
of having them make modifications exclusively for you, or provide you
|
||||||
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
|
with facilities for running those works, provided that you comply with
|
||||||
prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
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
|
Conveying under any other circumstances is permitted solely under
|
||||||
allowed; section 10 makes it unnecessary.
|
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
|
No covered work shall be deemed part of an effective technological
|
||||||
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
|
measure under any applicable law fulfilling obligations under article
|
||||||
restricting circumvention of such measures.
|
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
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
|
circumvention of technological measures to the extent such circumvention
|
||||||
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
|
is effected by exercising rights under this License with respect to
|
||||||
your or third parties' legal rights to forbid circumvention of technological measures.
|
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
|
You may convey verbatim copies of the Program's source code as you
|
||||||
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
|
receive it, in any medium, provided that you conspicuously and
|
||||||
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
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
|
You may charge any price or no price for each copy that you convey,
|
||||||
a fee.
|
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
|
You may convey a work based on the Program, or the modifications to
|
||||||
code under the terms of section 4, provided that you also meet all of these conditions:
|
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
|
a) The work must carry prominent notices stating that you modified
|
||||||
it, and giving a relevant date.
|
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
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
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
|
A compilation of a covered work with other separate and independent
|
||||||
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
|
works, which are not by their nature extensions of the covered work,
|
||||||
distribution medium, is called an
|
and which are not combined with it such as to form a larger program,
|
||||||
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
this License to apply to the other parts of the aggregate.
|
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
|
You may convey a covered work in object code form under the terms
|
||||||
machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
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
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by the
|
(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
|
Source of the work are being offered to the general public at no
|
||||||
charge under subsection 6d.
|
charge under subsection 6d.
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
|
A separable portion of the object code, whose source code is excluded
|
||||||
need not be included in conveying the object code work.
|
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
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
|
tangible personal property which is normally used for personal, family,
|
||||||
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
|
product received by a particular user, "normally used" refers to a
|
||||||
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
|
typical or common use of that class of product, regardless of the status
|
||||||
the product.
|
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
|
"Installation Information" for a User Product means any methods,
|
||||||
required to install and execute modified versions of a covered work in that User Product from a modified version of its
|
procedures, authorization keys, or other information required to install
|
||||||
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
|
and execute modified versions of a covered work in that User Product from
|
||||||
is in no case prevented or interfered with solely because modification has been made.
|
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
|
If you convey an object code work under this section in, or with, or
|
||||||
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
|
part of a transaction in which the right of possession and use of the
|
||||||
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
example, the work has been installed in ROM).
|
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
|
The requirement to provide Installation Information does not include a
|
||||||
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
|
requirement to continue to provide support service, warranty, or updates
|
||||||
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
|
for a work that has been modified or installed by the recipient, or for
|
||||||
and adversely affects the operation of the network or violates the rules and protocols for communication across the
|
the User Product in which it has been modified or installed. Access to a
|
||||||
network.
|
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
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
that is publicly documented (and with an implementation available to the public in source code form), and must require
|
in accord with this section must be in a format that is publicly
|
||||||
no special password or key for unpacking, reading or copying.
|
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
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
|
License by making exceptions from one or more of its conditions.
|
||||||
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
|
Additional permissions that are applicable to the entire Program shall
|
||||||
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
|
be treated as though they were included in this License, to the extent
|
||||||
governed by this License without regard to the additional permissions.
|
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
|
When you convey a copy of a covered work, you may at your option
|
||||||
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
|
remove any additional permissions from that copy, or from any part of
|
||||||
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
|
it. (Additional permissions may be written to require their own
|
||||||
or can give appropriate copyright permission.
|
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
|
Notwithstanding any other provision of this License, for material you
|
||||||
the copyright holders of that material) supplement the terms of this License with terms:
|
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
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
terms of sections 15 and 16 of this License; or
|
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
|
any liability that these contractual assumptions directly impose on
|
||||||
those licensors and authors.
|
those licensors and authors.
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
|
All other non-permissive additional terms are considered "further
|
||||||
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
|
received it, or any part of it, contains a notice stating that it is
|
||||||
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
|
governed by this License along with a term that is a further
|
||||||
that license document, provided that the further restriction does not survive such relicensing or conveying.
|
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
|
If you add terms to a covered work in accord with this section, you
|
||||||
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
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
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
exceptions; the above requirements apply either way.
|
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
|
You may not propagate or modify a covered work except as expressly
|
||||||
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
licenses granted under the third paragraph of section 11).
|
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
|
However, if you cease all violation of this License, then your
|
||||||
reinstated (a)
|
license from a particular copyright holder is reinstated (a)
|
||||||
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
after the cessation.
|
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
|
Moreover, your license from a particular copyright holder is
|
||||||
of the violation by some reasonable means, this is the first time you have received notice of violation of this
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
|
violation by some reasonable means, this is the first time you have
|
||||||
notice.
|
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
|
Termination of your rights under this section does not terminate the
|
||||||
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
|
licenses of parties who have received copies or rights from you under
|
||||||
qualify to receive new licenses for the same material under section 10.
|
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
|
You are not required to accept this License in order to receive or
|
||||||
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
|
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.
|
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,
|
Each time you convey a covered work, the recipient automatically
|
||||||
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
|
receives a license from the original licensors, to run, modify and
|
||||||
parties with this License.
|
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
|
An "entity transaction" is a transaction transferring control of an
|
||||||
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
|
organization, or substantially all assets of one, or subdividing an
|
||||||
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
|
organization, or merging organizations. If propagation of a covered
|
||||||
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
|
work results from an entity transaction, each party to that
|
||||||
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
|
transaction who receives a copy of the work also receives whatever
|
||||||
reasonable efforts.
|
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
|
You may not impose any further restrictions on the exercise of the
|
||||||
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
|
rights granted or affirmed under this License. For example, you may
|
||||||
and you may not initiate litigation
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
|
rights granted under this License, and you may not initiate litigation
|
||||||
selling, offering for sale, or importing the Program or any portion of it.
|
(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
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
Program is based. The work thus licensed is called the contributor's "contributor version".
|
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
|
A contributor's "essential patent claims" are all patent claims
|
||||||
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
|
by this License, of making, using, or selling its contributor version,
|
||||||
sublicenses in a manner consistent with the requirements of this License.
|
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
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
|
patent license under the contributor's essential patent claims, to
|
||||||
contributor version.
|
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
|
In the following three paragraphs, a "patent license" is any express
|
||||||
to enforce a patent
|
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
|
(such as an express permission to practice a patent or covenant not to
|
||||||
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
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
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
|
and the Corresponding Source of the work is not available for anyone
|
||||||
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
|
to copy, free of charge and under the terms of this License, through a
|
||||||
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
|
publicly available network server or other readily accessible means,
|
||||||
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
that country that you have reason to believe are valid.
|
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
|
If, pursuant to or in connection with a single transaction or
|
||||||
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
|
covered work, and grant a patent license to some of the parties
|
||||||
automatically extended to all recipients of the covered work and works based on it.
|
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,
|
A patent license is "discriminatory" if it does not include within
|
||||||
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
|
specifically granted under this License. You may not convey a covered
|
||||||
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
|
work if you are a party to an arrangement with a third party that is
|
||||||
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
|
in the business of distributing software, under which you make payment
|
||||||
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
|
to the third party based on the extent of your activity of conveying
|
||||||
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
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
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
infringement that may otherwise be available to you under applicable patent law.
|
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
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
would be to refrain entirely from conveying the Program.
|
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
|
Notwithstanding any other provision of this License, you have
|
||||||
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
|
permission to link or combine any covered work with a work licensed
|
||||||
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
|
combined work, and to convey the resulting work. The terms of this
|
||||||
to the combination as such.
|
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
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
|
the GNU General Public License from time to time. Such new versions will
|
||||||
problems or concerns.
|
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
|
Each version is given a distinguishing version number. If the
|
||||||
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
|
Program specifies that a certain numbered version of the GNU General
|
||||||
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
|
Public License "or any later version" applies to it, you have the
|
||||||
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
|
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.
|
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,
|
If the Program specifies that a proxy can decide which future
|
||||||
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
Program.
|
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
|
Later license versions may give you additional or different
|
||||||
on any author or copyright holder as a result of your choosing to follow a later version.
|
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
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
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
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
DAMAGES.
|
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
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
|
above cannot be given local legal effect according to their terms,
|
||||||
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
|
reviewing courts shall apply local law that most closely approximates
|
||||||
in return for a fee.
|
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
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
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
|
If you develop a new program, and you want it to be of the greatest
|
||||||
this is to make it free software which everyone can redistribute and change under these terms.
|
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
|
To do so, attach the following notices to the program. It is safest
|
||||||
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
|
to attach them to the start of each source file to most effectively
|
||||||
to where the full notice is found.
|
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.>
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
Copyright (C) <year> <name of author>
|
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.
|
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>
|
<program> Copyright (C) <year> <name of author>
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
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
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
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
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
|
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/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
|
The GNU General Public License does not permit incorporating your program
|
||||||
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
|
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>.
|
<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
|
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,
|
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
|
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)
|
# [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 |
|
| Platform | Artifact | Minecraft | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set |
|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.3 | Full feature set |
|
||||||
| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout |
|
| Folia | plugin jar | 26.1.2 - 26.3 | 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 |
|
| 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.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
|
||||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 |
|
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
|
||||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 |
|
| 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.
|
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
|
./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
|
Iris v<version> [CraftBukkit] <mc>.jar
|
||||||
@@ -261,16 +263,26 @@ Iris v<version> [Forge] <mc>+<loader>.jar
|
|||||||
Iris v<version> [NeoForge] <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
|
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
||||||
`./gradlew :spi:jar`.
|
`./gradlew :spi:jar`.
|
||||||
|
|
||||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
`./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/`
|
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`
|
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/`.
|
`../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.
|
If you need help compiling as a developer or contributor, ask in the Discord.
|
||||||
|
|
||||||
## Adapters / modded development
|
## Adapters / modded development
|
||||||
@@ -298,9 +310,10 @@ properties:
|
|||||||
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
|
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
|
||||||
|
|
||||||
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
|
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
|
||||||
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
|
Shared code lives in `adapters/modded-common` (loaders + the shared test suite) and
|
||||||
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
|
`adapters/client-common` (client HUD and world-type screens); every adapter adds those source
|
||||||
screens); every adapter adds those source directories, so one edit reaches all three loaders.
|
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
|
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
|
`-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.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainColumns;
|
||||||
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
|
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.Engine;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
@@ -24,7 +24,7 @@ import java.util.Optional;
|
|||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
import static org.mockito.Mockito.mock;
|
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.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
|
||||||
public final class IrisChunkGeneratorHeightCacheTest {
|
public final class IrisChunkGeneratorHeightCacheTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -44,68 +45,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
|
||||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
|
||||||
|
|
||||||
for (int index = 0; index < 100; index++) {
|
for (int index = 0; index < 100; index++) {
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
-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);
|
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
verify(fixture.engine(), never()).getComplex();
|
||||||
|
verify(fixture.engine(), never()).getMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
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, 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(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(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(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
|
||||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
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
|
@Test
|
||||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
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);
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
for (int repeat = 0; repeat < 2; repeat++) {
|
for (int repeat = 0; repeat < 2; repeat++) {
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
assertEquals(-11, 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, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
assertEquals(-13, 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, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||||
LevelHeightAccessor.create(-16, 2), null));
|
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));
|
LevelHeightAccessor.create(-14, 3), null));
|
||||||
}
|
}
|
||||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
verify(session, times(6)).resolvedColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Fixture fixture() throws Exception {
|
private static Fixture fixture() throws Exception {
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
IrisComplex complex = mock(IrisComplex.class);
|
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
|
||||||
when(engine.getCacheID()).thenReturn(1);
|
when(engine.getCacheID()).thenReturn(1);
|
||||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
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);
|
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||||
return new Fixture(generator, engine, complex);
|
return generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
|
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);
|
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.NativeBlockResolver;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
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.NativeGenerationRegistryImpl;
|
||||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
@@ -49,7 +48,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
return access;
|
return access;
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Generation registry contract requires loaded native registries");
|
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,
|
nativeAccess = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(registries,
|
||||||
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
|
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
|
||||||
message -> { throw new IllegalStateException("Unavailable native registry: " + message); }));
|
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.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
|
||||||
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
|
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.Engine;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -24,7 +25,7 @@ import java.util.Optional;
|
|||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
import static org.mockito.Mockito.mock;
|
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.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
|
||||||
public final class IrisChunkGeneratorHeightCacheTest {
|
public final class IrisChunkGeneratorHeightCacheTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -44,69 +46,78 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
|
||||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
|
||||||
|
|
||||||
for (int index = 0; index < 100; index++) {
|
for (int index = 0; index < 100; index++) {
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
-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);
|
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
verify(fixture.engine(), never()).getComplex();
|
||||||
|
verify(fixture.engine(), never()).getMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
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, 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(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(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(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
|
||||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
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
|
@Test
|
||||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
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);
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
for (int repeat = 0; repeat < 2; repeat++) {
|
for (int repeat = 0; repeat < 2; repeat++) {
|
||||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
assertEquals(-11, 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, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
assertEquals(-13, 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, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||||
LevelHeightAccessor.create(-16, 2), null));
|
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));
|
LevelHeightAccessor.create(-14, 3), null));
|
||||||
}
|
}
|
||||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
verify(session, times(6)).resolvedColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Fixture fixture() throws Exception {
|
private static Fixture fixture() throws Exception {
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
IrisComplex complex = mock(IrisComplex.class);
|
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
|
||||||
when(engine.getCacheID()).thenReturn(1);
|
when(engine.getCacheID()).thenReturn(1);
|
||||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
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);
|
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||||
return new Fixture(generator, engine, complex);
|
return generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
|
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);
|
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.RotatedPillarBlock;
|
||||||
import net.minecraft.world.level.block.LiquidBlock;
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||||
String key = "itemsadder:rocks/ruby_ore";
|
String key = "itemsadder:rocks/ruby_ore";
|
||||||
|
|||||||
+17
@@ -1,21 +1,30 @@
|
|||||||
package art.arcane.iris.nativegen.v26_3_R1;
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.tags.BlockTags;
|
import net.minecraft.tags.BlockTags;
|
||||||
import net.minecraft.tags.TagLoader;
|
import net.minecraft.tags.TagLoader;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public final class NativeBlockTags {
|
public final class NativeBlockTags {
|
||||||
|
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
|
||||||
|
|
||||||
private NativeBlockTags() {
|
private NativeBlockTags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bindHeightmapFixtures() {
|
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(),
|
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
||||||
List<Holder<Block>> canopy = List.of(Blocks.STONE.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, canopy,
|
||||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
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.resources.RegistryOps;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
|
import net.minecraft.world.attribute.modifier.MobSpawnSettingsModifier;
|
||||||
import net.minecraft.world.entity.MobCategory;
|
import net.minecraft.world.entity.MobCategory;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
@@ -53,5 +54,7 @@ public class CustomBiomeCodecTest {
|
|||||||
assertEquals(2, decoded.count().minInclusive());
|
assertEquals(2, decoded.count().minInclusive());
|
||||||
assertEquals(4, decoded.count().maxInclusive());
|
assertEquals(4, decoded.count().maxInclusive());
|
||||||
assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF);
|
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.IglooPieces;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
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.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||||
@@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class HeadlessChunkGeneratorTest {
|
public class HeadlessChunkGeneratorTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
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 net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessChunkLightingTest {
|
public final class HeadlessChunkLightingTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class HeadlessChunkSerializationTest {
|
public class HeadlessChunkSerializationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
-3
@@ -1,7 +1,6 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
|
||||||
import net.minecraft.data.registries.VanillaRegistries;
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -22,8 +21,6 @@ public final class HeadlessGenerationRegistryTest {
|
|||||||
HeadlessNativeTestRegistries::get);
|
HeadlessNativeTestRegistries::get);
|
||||||
PlatformGenerationRegistry registry = platform.generationRegistry();
|
PlatformGenerationRegistry registry = platform.generationRegistry();
|
||||||
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
|
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
|
||||||
assertEquals(new NativeWorldGenerationImpl().generationRendererIdentity(),
|
|
||||||
registry.generatedDefinitionRendererIdentity());
|
|
||||||
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
|
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
|
||||||
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
|
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
|
||||||
assertEquals(registry.canonicalDefinition("minecraft:dimension_type", "iris:native_test", dimension),
|
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 java.util.Map;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeBootstrapTest {
|
public final class HeadlessNativeBootstrapTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
||||||
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
||||||
|
|||||||
+4
@@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistriesTest {
|
public final class HeadlessNativeRegistriesTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
||||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
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 net.minecraft.core.registries.Registries;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistryTest {
|
public final class HeadlessNativeRegistryTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
|||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.data.registries.VanillaRegistries;
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeTileTest {
|
public final class HeadlessNativeTileTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -17,6 +17,7 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.spigotmc.SpigotWorldConfig;
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
@@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessRegionTerrainTest {
|
public final class HeadlessRegionTerrainTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
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.RegionFile;
|
||||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class NativeRegionTerrainWriterTest {
|
public final class NativeRegionTerrainWriterTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
|
|||||||
+4
@@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class OfflineNativeFeatureFeasibilityTest {
|
public final class OfflineNativeFeatureFeasibilityTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
private static HeadlessNativeRegistries loaded;
|
private static HeadlessNativeRegistries loaded;
|
||||||
private static RegistryAccess registries;
|
private static RegistryAccess registries;
|
||||||
private static DimensionType dimension;
|
private static DimensionType dimension;
|
||||||
|
|||||||
+4
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
@@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
public final class OfflineRegionPublicationTest {
|
public final class OfflineRegionPublicationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
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 mainClass = 'art.arcane.iris.Iris'
|
||||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||||
|
|
||||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
|
||||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
|
||||||
.get()
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(':core'))
|
compileOnly(project(':core'))
|
||||||
compileOnly(libs.lombok)
|
compileOnly(libs.lombok)
|
||||||
annotationProcessor(libs.lombok)
|
annotationProcessor(libs.lombok)
|
||||||
compileOnly(volmLibCoordinate) {
|
compileOnly('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
compileOnly(libs.paper.api)
|
compileOnly(libs.paper.api)
|
||||||
@@ -23,7 +19,7 @@ dependencies {
|
|||||||
testImplementation(libs.bstats)
|
testImplementation(libs.bstats)
|
||||||
testImplementation(project(':core'))
|
testImplementation(project(':core'))
|
||||||
testImplementation(testFixtures(project(':core')))
|
testImplementation(testFixtures(project(':core')))
|
||||||
testImplementation(volmLibCoordinate) {
|
testImplementation('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
compileOnly(libs.placeholderApi)
|
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;
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
||||||
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
||||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
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.IrisStartupAdmissionListener;
|
||||||
import art.arcane.iris.world.BukkitWorldReconciler;
|
import art.arcane.iris.world.BukkitWorldReconciler;
|
||||||
import art.arcane.iris.world.IrisWorldGeneratorResolver;
|
import art.arcane.iris.world.IrisWorldGeneratorResolver;
|
||||||
import art.arcane.iris.world.IrisWorldStorage;
|
|
||||||
import art.arcane.iris.world.PendingWorldDeleteQueue;
|
import art.arcane.iris.world.PendingWorldDeleteQueue;
|
||||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||||
|
import art.arcane.iris.world.WorldRefusalReporter;
|
||||||
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
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.ServerConfigurator;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
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.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
import org.bukkit.event.Listener;
|
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.BiomeProvider;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
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 terminalCleanupCompleted = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||||
private volatile boolean generatorDrainCompleted;
|
private volatile boolean generatorDrainCompleted;
|
||||||
|
private volatile boolean enableFailed;
|
||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
private volatile IrisPapiListener papiListener;
|
private volatile IrisPapiListener papiListener;
|
||||||
private volatile IrisPapiState papiState;
|
private volatile IrisPapiState papiState;
|
||||||
|
private volatile boolean papiLifecycleActive;
|
||||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||||
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
// 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.
|
// 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 PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||||
private BukkitLanguageSwitcher languageSwitcher;
|
private BukkitLanguageSwitcher languageSwitcher;
|
||||||
|
private IrisUpdateNotifier updateNotifier;
|
||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile Thread serverLifecycleThread;
|
|
||||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
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 plugin == null ? ComponentLog.discriminator("Iris", "&a") : plugin.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void enable() {
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
EnableTimings timings = new EnableTimings();
|
EnableTimings timings = new EnableTimings();
|
||||||
alreadyDrained.set(false);
|
alreadyDrained.set(false);
|
||||||
postStopFinisherStarted.set(false);
|
postStopFinisherStarted.set(false);
|
||||||
@@ -649,6 +642,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
||||||
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
||||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||||
|
updateNotifier = new IrisUpdateNotifier(this);
|
||||||
PaperLibBootstrap.install();
|
PaperLibBootstrap.install();
|
||||||
SimdSupport.install();
|
SimdSupport.install();
|
||||||
timings.mark("bootstrap");
|
timings.mark("bootstrap");
|
||||||
@@ -788,7 +782,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
});
|
});
|
||||||
timings.mark("startupTasks");
|
timings.mark("startupTasks");
|
||||||
timings.report();
|
timings.report();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class EnableTimings {
|
private static final class EnableTimings {
|
||||||
@@ -838,7 +831,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
public void addShutdownHook() {
|
public void addShutdownHook() {
|
||||||
removeShutdownHook();
|
removeShutdownHook();
|
||||||
serverLifecycleThread = Thread.currentThread();
|
|
||||||
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||||
try {
|
try {
|
||||||
@@ -877,7 +869,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
*/
|
*/
|
||||||
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
||||||
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, 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() {
|
public PendingWorldReplacementManager pendingWorldReplacements() {
|
||||||
@@ -910,20 +909,25 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
IrisPlatforms.bind(new BukkitPlatform());
|
enableFailed = false;
|
||||||
IrisStartupValidation.begin();
|
IrisStartupValidation.begin();
|
||||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||||
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
|
||||||
pendingWorldReplacements.registerPlatformEntryListener();
|
|
||||||
boolean enabled;
|
|
||||||
try {
|
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) {
|
} catch (Throwable failure) {
|
||||||
refuseVanillaFallback(failure);
|
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||||
throw failure;
|
generatorResolver.refusals().startupWorldsCreated())) {
|
||||||
}
|
throw failure;
|
||||||
if (!enabled) {
|
}
|
||||||
refuseVanillaFallback(null);
|
enableFailed = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BukkitGuiHost.install();
|
BukkitGuiHost.install();
|
||||||
@@ -936,57 +940,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
papiLifecycleActive = true;
|
||||||
|
setupPapi();
|
||||||
reportLockedRuntime();
|
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() {
|
private static void reportLockedRuntime() {
|
||||||
String denial = IrisStartupValidation.denialReason().orElse(null);
|
String denial = IrisStartupValidation.denialReason().orElse(null);
|
||||||
if (denial == null) {
|
if (denial == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean managedStorage;
|
for (String line : RuntimeLockNotice.compose(denial, FailedEnableLock.irisWorldsPresent())) {
|
||||||
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)) {
|
|
||||||
Iris.error(line);
|
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() {
|
public BukkitDebugDump debugDump() {
|
||||||
return debugDump;
|
return debugDump;
|
||||||
}
|
}
|
||||||
@@ -1004,6 +988,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
|
if (updateNotifier != null) {
|
||||||
|
updateNotifier.close();
|
||||||
|
updateNotifier = null;
|
||||||
|
}
|
||||||
if (debugDump != null) {
|
if (debugDump != null) {
|
||||||
debugDump.close();
|
debugDump.close();
|
||||||
debugDump = null;
|
debugDump = null;
|
||||||
@@ -1014,11 +1004,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
IrisLanguage.shutdown();
|
IrisLanguage.shutdown();
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
boolean serverStopping = IrisToolbelt.isServerStopping();
|
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping());
|
||||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||||
if (restartingAtStartupBoundary) {
|
if (restartingAtStartupBoundary) {
|
||||||
teardownRuntime("startup-boundary-restart", 30L);
|
teardownRuntime("startup-boundary-restart", 30L);
|
||||||
} else if (serverStopping) {
|
} else if (deferToServerStop) {
|
||||||
quiesceRuntimeForServerShutdown("onDisable");
|
quiesceRuntimeForServerShutdown("onDisable");
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
} else {
|
} else {
|
||||||
@@ -1036,26 +1026,29 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||||
super.onDisable();
|
super.onDisable();
|
||||||
if (!serverStopping || restartingAtStartupBoundary) {
|
if (!deferToServerStop || restartingAtStartupBoundary) {
|
||||||
finishTerminalCleanup();
|
finishTerminalCleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping()) {
|
if (IrisToolbelt.isServerStopping()) {
|
||||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
if (alreadyDrained.get()) {
|
if (alreadyDrained.get()) {
|
||||||
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
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.");
|
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
||||||
drainOnce("pre-unload:" + reason, 45L);
|
drainOnce("pre-unload:" + reason, 45L);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1169,7 +1162,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (generator == null) {
|
if (generator == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IrisToolbelt.beginWorldMaintenance(world, reason, true);
|
IrisToolbelt.beginWorldMaintenance(world, reason);
|
||||||
if (!deferredShutdownGenerators.contains(generator)) {
|
if (!deferredShutdownGenerators.contains(generator)) {
|
||||||
deferredShutdownGenerators.add(generator);
|
deferredShutdownGenerators.add(generator);
|
||||||
}
|
}
|
||||||
@@ -1181,52 +1174,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Thread activeServerThread = serverLifecycleThread;
|
Thread finisher = new Thread(() -> finishRuntimeAfterServerShutdown("post-server-stop"),
|
||||||
if (activeServerThread == null) {
|
"Iris-PostStop-Finisher");
|
||||||
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");
|
|
||||||
finisher.setDaemon(false);
|
finisher.setDaemon(false);
|
||||||
finisher.start();
|
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() {
|
private void runShutdownHook() {
|
||||||
if (startupBoundaryRestart.get()) {
|
if (startupBoundaryRestart.get()) {
|
||||||
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
finishRuntimeAfterServerShutdown("shutdown-hook");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRuntimeAfterServerShutdown(String reason) {
|
||||||
if (!awaitServerShutdownBoundary()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
finishDeferredRuntimeTeardown(reason, 30L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean awaitServerShutdownBoundary() {
|
private boolean awaitServerShutdownBoundary() {
|
||||||
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||||
if (boundary == null) {
|
if (boundary == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return boundary.await(
|
return boundary.await(
|
||||||
@@ -1290,6 +1263,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
&& preservation.hasActiveResources();
|
&& 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) {
|
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||||
List<World> irisWorlds = new ArrayList<>();
|
List<World> irisWorlds = new ArrayList<>();
|
||||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||||
@@ -1344,8 +1331,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPapi() {
|
@EventHandler
|
||||||
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,7 +1374,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
papiRegistration = registration;
|
papiRegistration = registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teardownPapi() {
|
private synchronized void teardownPapi() {
|
||||||
IrisPapiListener listener = papiListener;
|
IrisPapiListener listener = papiListener;
|
||||||
papiListener = null;
|
papiListener = null;
|
||||||
|
|
||||||
@@ -1457,6 +1459,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
return generatorResolver.resolveDefaultWorldGenerator(worldName, id);
|
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() {
|
public void splash() {
|
||||||
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
||||||
printPacks();
|
printPacks();
|
||||||
@@ -1476,12 +1486,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
public int getIrisVersion() {
|
public int getIrisVersion() {
|
||||||
String input = getDescription().getVersion();
|
String input = getDescription().getVersion();
|
||||||
int hyphenIndex = input.indexOf('-');
|
int hyphenIndex = input.indexOf('-');
|
||||||
if (hyphenIndex != -1) {
|
String release = hyphenIndex < 0 ? input : input.substring(0, hyphenIndex);
|
||||||
String result = input.substring(0, hyphenIndex);
|
if (!release.matches("[0-9]+(?:\\.[0-9]+)*")) {
|
||||||
result = result.replaceAll("\\.", "");
|
return -1;
|
||||||
return Integer.parseInt(result);
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(release.replace(".", ""));
|
||||||
|
} catch (NumberFormatException invalid) {
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMCVersion() {
|
public int getMCVersion() {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
|
|||||||
import io.papermc.paper.ServerBuildInfo;
|
import io.papermc.paper.ServerBuildInfo;
|
||||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
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.BootstrapContext;
|
||||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
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.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
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.
|
* Stops startup when a world the server is about to load carries Iris storage Iris cannot generate from.
|
||||||
* <p>
|
* <p>
|
||||||
* The server enumerates levels from {@code <levelRoot>/dimensions/<namespace>/<key>} on disk, so a world
|
* 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
|
* whose folder is present is going to be loaded whatever Iris does. The generator resolver answers a world
|
||||||
* {@code getDefaultWorldGenerator} throws and falls back to the vanilla generator, which writes vanilla
|
* it cannot generate with a fail-closed generator, which stops startup at that world's own level creation,
|
||||||
* terrain straight into that world's region files. A pack snapshot that has gone missing therefore has
|
* after the levels before it have loaded. A pack snapshot that has gone missing is found here instead,
|
||||||
* to be found here, before any level is created, exactly as a corrupt {@code dimensions/<id>.json} is.
|
* 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>
|
* <p>
|
||||||
* A world with no folder at all is not a startup failure: nothing enumerates it, nothing loads it, and
|
* 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
|
* 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
|
* 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,
|
* 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 {
|
static void requireUsableWorldStorage(BukkitStartupPaths startupPaths) throws IOException {
|
||||||
Path levelRoot = startupPaths.levelRoot();
|
Path levelRoot = startupPaths.levelRoot();
|
||||||
@@ -135,6 +139,7 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
levelRoot
|
levelRoot
|
||||||
);
|
);
|
||||||
StringBuilder unusable = new StringBuilder();
|
StringBuilder unusable = new StringBuilder();
|
||||||
|
StringBuilder unbound = new StringBuilder();
|
||||||
for (IrisWorldStorageEntry entry : entries) {
|
for (IrisWorldStorageEntry entry : entries) {
|
||||||
switch (entry.state()) {
|
switch (entry.state()) {
|
||||||
case PRESENT -> {
|
case PRESENT -> {
|
||||||
@@ -152,15 +157,29 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
.append(": ")
|
.append(": ")
|
||||||
.append(entry.detail() == null ? "storage is unusable" : entry.detail())
|
.append(entry.detail() == null ? "storage is unusable" : entry.detail())
|
||||||
.append(')');
|
.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()) {
|
List<String> refusals = new ArrayList<>(2);
|
||||||
return;
|
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) {
|
static void armStartupFailure(BootstrapContext context, Throwable failure) {
|
||||||
@@ -175,17 +194,30 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
context.getLifecycleManager().registerEventHandler(eventType, event -> {
|
context.getLifecycleManager().registerEventHandler(eventType, event -> {
|
||||||
throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure);
|
throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure);
|
||||||
});
|
});
|
||||||
|
List<String> notice = startupRefusalNotice(failure);
|
||||||
try {
|
try {
|
||||||
context.getLogger().error(
|
for (String line : notice) {
|
||||||
"Iris bootstrap failed; registry and world startup will be stopped at datapack discovery.",
|
context.getLogger().error(line);
|
||||||
failure
|
}
|
||||||
);
|
context.getLogger().warn("Iris bootstrap failure", failure);
|
||||||
} catch (Throwable loggingFailure) {
|
} catch (Throwable loggingFailure) {
|
||||||
|
notice.forEach(System.err::println);
|
||||||
failure.addSuppressed(loggingFailure);
|
failure.addSuppressed(loggingFailure);
|
||||||
failure.printStackTrace(System.err);
|
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) {
|
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
|
||||||
try {
|
try {
|
||||||
return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest(
|
return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package art.arcane.iris.api.tree;
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
public interface TreeFellerRunHooks {
|
public interface TreeFellerRunHooks {
|
||||||
|
default boolean requiresSneaking() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
|
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
|
||||||
@Override
|
@Override
|
||||||
public void onActivationAccepted() {
|
public void onActivationAccepted() {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
|
|||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
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.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
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")
|
@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 {
|
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(
|
public void biome(
|
||||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||||
IrisBiome biome,
|
IrisBiome biome,
|
||||||
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
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")
|
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||||
public void region(
|
public void region(
|
||||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
@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;
|
return;
|
||||||
}
|
}
|
||||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
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 {
|
try {
|
||||||
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
||||||
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
||||||
@@ -523,6 +525,8 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
if (!Boolean.TRUE.equals(evacuated)) {
|
if (!Boolean.TRUE.equals(evacuated)) {
|
||||||
return CompletableFuture.completedFuture(false);
|
return CompletableFuture.completedFuture(false);
|
||||||
}
|
}
|
||||||
|
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
||||||
|
maintenanceEntered.set(true);
|
||||||
return WorldLifecycleService.get().unloadAsync(world, true);
|
return WorldLifecycleService.get().unloadAsync(world, true);
|
||||||
})
|
})
|
||||||
.thenCompose(unloaded -> {
|
.thenCompose(unloaded -> {
|
||||||
@@ -537,7 +541,9 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
});
|
});
|
||||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||||
.whenComplete((unloaded, throwable) -> {
|
.whenComplete((unloaded, throwable) -> {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
if (maintenanceEntered.get()) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
|
}
|
||||||
lease.close();
|
lease.close();
|
||||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||||
@@ -546,7 +552,9 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
J.s(response);
|
J.s(response);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
if (maintenanceEntered.get()) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
|
}
|
||||||
lease.close();
|
lease.close();
|
||||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
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);
|
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
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))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
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()))));
|
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);
|
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||||
return;
|
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))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
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()))));
|
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);
|
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||||
return;
|
return;
|
||||||
@@ -363,47 +363,79 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
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 {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packFolder);
|
if (ticket == null) {
|
||||||
PackValidationRegistry.publish(result);
|
throw new IllegalStateException("Pack content is currently being changed.");
|
||||||
reportResult(s, result);
|
}
|
||||||
return result;
|
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) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||||
? e.getClass().getSimpleName()
|
? e.getClass().getSimpleName()
|
||||||
: e.getMessage();
|
: e.getMessage();
|
||||||
PackValidationResult result = new PackValidationResult(
|
result = new PackValidationResult(
|
||||||
packFolder.getName(),
|
packFolder.getName(),
|
||||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||||
List.of(),
|
List.of(),
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
PackValidationRegistry.publish(result);
|
contentFingerprint = "";
|
||||||
reportResult(s, result);
|
contextFingerprint = "";
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private void persistValidationCache(File packsRoot) {
|
||||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
try {
|
||||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
for (File packDirectory : packDirectories) {
|
PackFingerprints.PackContentSnapshot snapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
if (result == null) {
|
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||||
// The boot cache only loads when it covers every pack, so a partial write is
|
for (File packDirectory : packDirectories) {
|
||||||
// pointless - but say so instead of silently skipping the persist forever.
|
PackValidationResult result = PackValidationRegistry.getMatching(
|
||||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
packDirectory.toPath(), snapshot.packContents().get(packDirectory.getName()));
|
||||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
results.add(result);
|
for (int index = 0; index < packDirectories.size(); index++) {
|
||||||
}
|
File packDirectory = packDirectories.get(index);
|
||||||
try {
|
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(
|
PackValidationCache.save(
|
||||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||||
PackValidationCache.contentFingerprint(packsRoot),
|
snapshot.content(),
|
||||||
PackValidationCache.contextFingerprint(),
|
contextFingerprint,
|
||||||
results);
|
results);
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
Iris.reportError("Could not persist refreshed pack-validation results", 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.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.inventory.InventoryType;
|
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
@@ -309,23 +308,33 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
O<Integer> ta = new O<>();
|
O<Integer> ta = new O<>();
|
||||||
ta.set(-1);
|
ta.set(-1);
|
||||||
|
|
||||||
var sender = sender();
|
VolmitSender sender = sender();
|
||||||
var player = player();
|
Player player = player();
|
||||||
var engine = engine();
|
Engine engine = engine();
|
||||||
|
|
||||||
ta.set(J.sr(() ->
|
ta.set(J.sr(() -> {
|
||||||
{
|
if (!J.runEntity(player, () -> {
|
||||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
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());
|
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));
|
}, fast ? 5 : 35));
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now);
|
double chunksPerSecond = registered.generationRate.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now));
|
worldSnapshots.add(EngineTelemetrySnapshot.capture(
|
||||||
|
engine,
|
||||||
|
chunksPerSecond,
|
||||||
|
registered.generationRate.generationTimingsFresh(now),
|
||||||
|
now));
|
||||||
registered.clearMetricsFailure();
|
registered.clearMetricsFailure();
|
||||||
} catch (Throwable exception) {
|
} catch (Throwable exception) {
|
||||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||||
@@ -552,6 +556,7 @@ public final class IrisEngineSVC implements IrisService {
|
|||||||
progress.generated(),
|
progress.generated(),
|
||||||
progress.totalChunks(),
|
progress.totalChunks(),
|
||||||
progress.chunksRemaining(),
|
progress.chunksRemaining(),
|
||||||
|
progress.chunksInFlight(),
|
||||||
progress.chunksPerSecond(),
|
progress.chunksPerSecond(),
|
||||||
progress.eta(),
|
progress.eta(),
|
||||||
progress.elapsed(),
|
progress.elapsed(),
|
||||||
@@ -612,8 +617,7 @@ public final class IrisEngineSVC implements IrisService {
|
|||||||
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
|
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
|
||||||
private volatile ScheduledFuture<?> maintenance;
|
private volatile ScheduledFuture<?> maintenance;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
private long lastGeneratedSampleAtMs;
|
private final GenerationRateWindow generationRate = new GenerationRateWindow();
|
||||||
private int lastGeneratedCount;
|
|
||||||
|
|
||||||
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
|
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
|
||||||
name = registration.name();
|
name = registration.name();
|
||||||
@@ -696,19 +700,6 @@ public final class IrisEngineSVC implements IrisService {
|
|||||||
return activeMaintenance.get() != null;
|
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() {
|
private void close() {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+3
@@ -101,6 +101,7 @@ public record IrisTelemetrySnapshot(
|
|||||||
long generated,
|
long generated,
|
||||||
long total,
|
long total,
|
||||||
long remaining,
|
long remaining,
|
||||||
|
long inFlight,
|
||||||
double chunksPerSecond,
|
double chunksPerSecond,
|
||||||
long etaMs,
|
long etaMs,
|
||||||
long elapsedMs,
|
long elapsedMs,
|
||||||
@@ -115,6 +116,7 @@ public record IrisTelemetrySnapshot(
|
|||||||
0L,
|
0L,
|
||||||
0L,
|
0L,
|
||||||
0L,
|
0L,
|
||||||
|
0L,
|
||||||
0D,
|
0D,
|
||||||
0L,
|
0L,
|
||||||
0L,
|
0L,
|
||||||
@@ -130,6 +132,7 @@ public record IrisTelemetrySnapshot(
|
|||||||
generated = Math.max(0L, generated);
|
generated = Math.max(0L, generated);
|
||||||
total = Math.max(0L, total);
|
total = Math.max(0L, total);
|
||||||
remaining = Math.max(0L, remaining);
|
remaining = Math.max(0L, remaining);
|
||||||
|
inFlight = Math.max(0L, inFlight);
|
||||||
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||||
etaMs = Math.max(0L, etaMs);
|
etaMs = Math.max(0L, etaMs);
|
||||||
elapsedMs = Math.max(0L, elapsedMs);
|
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.Iris;
|
||||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
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.IntegrationMetricDescriptor;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
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.IntegrationMetricSchema;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||||
@@ -30,7 +34,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
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 IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||||
new IntegrationProtocolVersion(1, 0),
|
new IntegrationProtocolVersion(1, 0),
|
||||||
@@ -41,6 +45,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
"handshake",
|
"handshake",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
IntegrationSnapshotProvider.CAPABILITY,
|
||||||
"metric-groups",
|
"metric-groups",
|
||||||
"iris-engine-metrics",
|
"iris-engine-metrics",
|
||||||
"iris-world-metrics"
|
"iris-world-metrics"
|
||||||
@@ -63,6 +68,9 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
);
|
);
|
||||||
|
|
||||||
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||||
|
private final IntegrationMetricPublisher snapshots = new IntegrationMetricPublisher(
|
||||||
|
IntegrationMetricSchema.irisKeys().size(), 30_000L);
|
||||||
|
private int snapshotTaskId = -1;
|
||||||
|
|
||||||
public IrisIntegrationService() {
|
public IrisIntegrationService() {
|
||||||
this(IrisIntegrationService::currentTelemetry);
|
this(IrisIntegrationService::currentTelemetry);
|
||||||
@@ -77,11 +85,17 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||||
|
snapshotTaskId = J.ar(this::publishSnapshots, 20);
|
||||||
Iris.verbose("Integration provider registered for Iris");
|
Iris.verbose("Integration provider registered for Iris");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (snapshotTaskId != -1) {
|
||||||
|
J.car(snapshotTaskId);
|
||||||
|
snapshotTaskId = -1;
|
||||||
|
}
|
||||||
|
snapshots.clear();
|
||||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +202,20 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
return Map.copyOf(selected);
|
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
|
@Override
|
||||||
public List<IntegrationMetricGroup> metricGroups() {
|
public List<IntegrationMetricGroup> metricGroups() {
|
||||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||||
@@ -319,6 +347,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
||||||
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
||||||
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_REMAINING,
|
||||||
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||||
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
||||||
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_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_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
|
||||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
||||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), 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_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
|
||||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
||||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), 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);
|
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
|
@Override
|
||||||
public CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed) {
|
public CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed) {
|
||||||
|
String configuredWorldName = worldKey.toString();
|
||||||
|
ChunkGenerator generator;
|
||||||
|
IrisDimension irisDimension;
|
||||||
try {
|
try {
|
||||||
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
|
configuredWorldName = configuredWorldName(worldKey);
|
||||||
String configuredWorldName = configuredWorldName(worldKey);
|
Iris.info("Loading World: %s | Generator: %s", IrisWorldStorage.logicalName(worldKey), dimension);
|
||||||
Iris.info("Loading World: %s | Generator: %s", logicalWorldName, dimension);
|
generator = plugin.requireWorldGenerator(configuredWorldName, dimension);
|
||||||
ChunkGenerator generator = plugin.getDefaultWorldGenerator(configuredWorldName, dimension);
|
irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
||||||
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
|
||||||
if (generator == null || irisDimension == null) {
|
if (generator == null || irisDimension == null) {
|
||||||
throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\".");
|
throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\".");
|
||||||
}
|
}
|
||||||
|
} catch (Throwable refusal) {
|
||||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + logicalWorldName + " using Iris:" + dimension + "...");
|
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)
|
WorldCreator creator = WorldCreatorCompat.ofPersistentKey(worldKey)
|
||||||
.generator(generator)
|
.generator(generator)
|
||||||
.environment(BukkitEnvironment.from(irisDimension.getEnvironment()));
|
.environment(BukkitEnvironment.from(irisDimension.getEnvironment()));
|
||||||
|
|||||||
+15
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.world;
|
package art.arcane.iris.world;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||||
import org.bukkit.HeightMap;
|
import org.bukkit.HeightMap;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
@@ -48,6 +49,20 @@ final class IrisFailClosedChunkGenerator extends ChunkGenerator {
|
|||||||
+ "' remains locked: " + denialReason);
|
+ "' 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
|
@Override
|
||||||
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
|
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
|
||||||
throw refusal();
|
throw refusal();
|
||||||
|
|||||||
+101
-26
@@ -18,8 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.world;
|
package art.arcane.iris.world;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
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.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
|
* 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 externalContentRefreshQueued = new AtomicBoolean();
|
||||||
private final AtomicBoolean externalContentRefreshRequested = new AtomicBoolean();
|
private final AtomicBoolean externalContentRefreshRequested = new AtomicBoolean();
|
||||||
private final Map<Path, PendingSnapshot> pendingSnapshots = new ConcurrentHashMap<>();
|
private final Map<Path, PendingSnapshot> pendingSnapshots = new ConcurrentHashMap<>();
|
||||||
|
private final WorldRefusalReporter refusals = new WorldRefusalReporter();
|
||||||
|
|
||||||
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
|
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
@@ -182,12 +185,12 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
PackFingerprints.PackContentSnapshot contentSnapshot =
|
||||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||||
String contextFingerprint = "";
|
String contextFingerprint = "";
|
||||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||||
try {
|
try {
|
||||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
contentSnapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
if (!externalContentChanged) {
|
if (!externalContentChanged) {
|
||||||
cached = PackValidationCache.load(
|
cached = PackValidationCache.load(
|
||||||
@@ -272,19 +275,19 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
private static FreshValidation validateStablePacks(
|
private static FreshValidation validateStablePacks(
|
||||||
File packsRoot,
|
File packsRoot,
|
||||||
List<File> initialPackDirs,
|
List<File> initialPackDirs,
|
||||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
PackFingerprints.PackContentSnapshot initialSnapshot
|
||||||
) {
|
) {
|
||||||
List<File> packDirs = initialPackDirs;
|
List<File> packDirs = initialPackDirs;
|
||||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
PackFingerprints.PackContentSnapshot before = initialSnapshot;
|
||||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
List<PackValidationResult> results = validatePacks(packDirs);
|
List<PackValidationResult> results = validatePacks(packDirs);
|
||||||
ServerConfigurator.PackContentSnapshot after;
|
PackFingerprints.PackContentSnapshot after;
|
||||||
try {
|
try {
|
||||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
after = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Iris.reportError("Could not verify Iris pack bytes after validation", 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<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||||
@@ -480,12 +483,28 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
return fallback.get();
|
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
|
@Nullable
|
||||||
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, @Nullable String id) {
|
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, @Nullable String id) {
|
||||||
if (isPlotSquaredGeneratorDiscoveryProbe(worldName, id)) {
|
if (isPlotSquaredGeneratorDiscoveryProbe(worldName, id)) {
|
||||||
Iris.debug("Ignoring PlotSquared generator discovery probe");
|
Iris.debug("Ignoring PlotSquared generator discovery probe");
|
||||||
return null;
|
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)) {
|
if (isGeneratorDiscoveryProbe(worldName, id)) {
|
||||||
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
||||||
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
||||||
@@ -505,21 +524,37 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
return startupLock;
|
return startupLock;
|
||||||
}
|
}
|
||||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
String dimension = id == null || id.isEmpty() ? IrisSettings.get().getGenerator().getDefaultWorldType() : id;
|
||||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
Iris.debug("Generator ID: " + dimension + " requested by bukkit/plugin");
|
||||||
|
|
||||||
File levelRoot = IrisWorldStorage.levelRoot();
|
File levelRoot = IrisWorldStorage.levelRoot();
|
||||||
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
||||||
requireWorldKeyAvailable(worldName, worldKey);
|
requireWorldKeyAvailable(worldName, worldKey);
|
||||||
requireOwnedWorld(worldName, levelRoot, 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 {
|
try {
|
||||||
return resolveFrozenWorldGenerator(worldName, id);
|
Iris.reportError(failure);
|
||||||
} catch (RuntimeException failure) {
|
} catch (Throwable loggingFailure) {
|
||||||
Iris.reportError("Refusing to load configured Iris world '" + worldName
|
System.err.println("[Iris] Could not log the refusal of '" + worldName + "': "
|
||||||
+ "' because its frozen world-local pack snapshot could not be used.", failure);
|
+ loggingFailure.getClass().getName());
|
||||||
Bukkit.shutdown();
|
}
|
||||||
throw failure;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
|
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
|
throw new IllegalStateException("'" + worldName + "' (" + messageKey
|
||||||
+ ") has no Iris world storage, so Iris cannot generate it."
|
+ ") has no Iris world storage, so Iris cannot generate it."
|
||||||
+ " Create Iris worlds with /iris create " + IrisWorldStorage.logicalName(messageKey)
|
+ " Create Iris worlds with /iris create " + IrisWorldStorage.logicalName(messageKey)
|
||||||
+ " type=<pack>; Iris registers them with Multiverse itself.");
|
+ " 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) {
|
private static boolean hasGenerationStorage(File dimensionRoot) {
|
||||||
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
|
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
|
||||||
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|
||||||
@@ -817,14 +896,10 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
|
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
|
||||||
return IrisPlatforms.get()
|
return dimension.dimensionTypeResourceKey(
|
||||||
.registries()
|
IrisPlatforms.get().registries().generationRegistry(),
|
||||||
.generationRegistry()
|
data.getDataFolder().getName()
|
||||||
.dimensionTypeResourceKey(
|
);
|
||||||
data.getDataFolder().getName(),
|
|
||||||
dimension.getLoadKey(),
|
|
||||||
dimension.getDimensionTypeKey()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record LoadedHistory(GenerationHistory history, File activePack) {
|
private record LoadedHistory(GenerationHistory history, File activePack) {
|
||||||
@@ -845,7 +920,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
|
|
||||||
private record FreshValidation(
|
private record FreshValidation(
|
||||||
List<File> packDirs,
|
List<File> packDirs,
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
PackFingerprints.PackContentSnapshot contentSnapshot,
|
||||||
List<PackValidationResult> results,
|
List<PackValidationResult> results,
|
||||||
boolean stable
|
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
|
@Override
|
||||||
public boolean shouldBypassMantleStages(Engine engine) {
|
public boolean shouldRefuseGeneration(Engine engine) {
|
||||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-4
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
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 {
|
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||||
private final TreeFellerSVC service;
|
private final TreeFellerSVC service;
|
||||||
private final FellingRun run;
|
private final FellingRun run;
|
||||||
|
private final ItemStack tool;
|
||||||
|
|
||||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
RoutedBlockBreakEvent(Options options) {
|
||||||
super(block, player);
|
super(options.block(), options.player());
|
||||||
this.run = run;
|
run = options.run();
|
||||||
this.service = service;
|
service = options.service();
|
||||||
|
tool = Objects.requireNonNull(options.tool(), "tool").clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ItemStack> captureNativeDrops() {
|
||||||
|
return getBlock().getDrops(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean routeDrop(Object drop) {
|
public boolean routeDrop(Object drop) {
|
||||||
return service.isServiceEnabled() && run.presentation.routeDrop(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)
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
|
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
|
||||||
if (!event.isSneaking()) {
|
if (!event.isSneaking()) {
|
||||||
runner.finishRuns(event.getPlayer().getUniqueId());
|
runner.finishSneakRuns(event.getPlayer().getUniqueId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-5
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
|
|||||||
return;
|
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);
|
service.managedEvents.add(probe);
|
||||||
try {
|
try {
|
||||||
Bukkit.getPluginManager().callEvent(probe);
|
Bukkit.getPluginManager().callEvent(probe);
|
||||||
@@ -523,10 +524,15 @@ final class TreeFellingRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunControlActive(FellingRun run, Player player) {
|
private boolean isRunControlActive(FellingRun run, Player player) {
|
||||||
return player.isOnline()
|
try {
|
||||||
&& player.getGameMode() == GameMode.SURVIVAL
|
return player.isOnline()
|
||||||
&& player.isSneaking()
|
&& player.getGameMode() == GameMode.SURVIVAL
|
||||||
&& player.getWorld().equals(run.candidate.world());
|
&& (!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) {
|
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(
|
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
|
||||||
List<TreeMarkerTraversal.Position> positions
|
List<TreeMarkerTraversal.Position> positions
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
|||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
description: More than a Dimension!
|
description: More than a Dimension!
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
default: op
|
||||||
@@ -25,34 +28,26 @@ permissions:
|
|||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
PlaceholderAPI:
|
PlaceholderAPI:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
CraftEngine:
|
CraftEngine:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Nexo:
|
Nexo:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Oraxen:
|
Oraxen:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
ItemsAdder:
|
ItemsAdder:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
SCore:
|
SCore:
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
ExecutableItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: false
|
|
||||||
MythicLib:
|
|
||||||
load: OMIT
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -61,11 +56,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
eco:
|
eco:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
EcoItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MythicMobs:
|
MythicMobs:
|
||||||
@@ -77,7 +68,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
KGenerators:
|
KGenerators:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Multiverse-Core:
|
Multiverse-Core:
|
||||||
@@ -85,6 +76,6 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
WorldEdit:
|
WorldEdit:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -3,18 +3,6 @@ version: ${version}
|
|||||||
main: ${main}
|
main: ${main}
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
softdepend:
|
|
||||||
- PlaceholderAPI
|
|
||||||
- CraftEngine
|
|
||||||
- Nexo
|
|
||||||
- Oraxen
|
|
||||||
- ItemsAdder
|
|
||||||
- SCore
|
|
||||||
- ExecutableItems
|
|
||||||
- eco
|
|
||||||
- EcoItems
|
|
||||||
- KGenerators
|
|
||||||
- WorldEdit
|
|
||||||
loadbefore: [ Multiverse-Core ]
|
loadbefore: [ Multiverse-Core ]
|
||||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
@@ -23,6 +11,9 @@ commands:
|
|||||||
iris:
|
iris:
|
||||||
aliases: [ ir, irs ]
|
aliases: [ ir, irs ]
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
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
|
version: 1.0.0
|
||||||
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
||||||
api-version: '26.1.2'
|
api-version: '26.1.2'
|
||||||
|
folia-supported: true
|
||||||
depend: [Iris]
|
depend: [Iris]
|
||||||
commands:
|
commands:
|
||||||
irislifecycle:
|
irislifecycle:
|
||||||
|
|||||||
+284
-7
@@ -1,11 +1,31 @@
|
|||||||
package art.arcane.iris.fixture;
|
package art.arcane.iris.fixture;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.volmlib.nativelib.NativeAdapters;
|
import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
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.IrisCreator;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.world.IrisToolbelt;
|
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.Material;
|
||||||
import org.bukkit.block.Biome;
|
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.NativeTerrainAccess;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
||||||
@@ -19,6 +39,10 @@ import org.bukkit.command.CommandSender;
|
|||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
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.concurrent.CompletableFuture;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -26,12 +50,16 @@ import java.util.logging.Level;
|
|||||||
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
||||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
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 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 static final long WORLD_SEED = 78264193L;
|
||||||
private NativeWorldRuntime runtime;
|
private NativeWorldRuntime runtime;
|
||||||
|
private TreasureLootChecks treasureLootChecks;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
||||||
|
treasureLootChecks = new TreasureLootChecks(this);
|
||||||
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
||||||
getLogger().info("Native lifecycle ready: " + runtime.description()
|
getLogger().info("Native lifecycle ready: " + runtime.description()
|
||||||
+ "; terrain provider=" + terrain.getClass().getName());
|
+ "; terrain provider=" + terrain.getClass().getName());
|
||||||
@@ -44,18 +72,40 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
if (arguments.length == 0) {
|
||||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload");
|
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
switch (arguments[0]) {
|
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 "create" -> create(sender);
|
||||||
case "unload" -> unload(sender);
|
case "unload" -> unload(sender);
|
||||||
case "structures" -> structures(sender);
|
case "structures" -> structures(sender);
|
||||||
case "terrain-create" -> createTerrain(sender);
|
case "terrain-create" -> createTerrain(sender);
|
||||||
case "terrain-check" -> checkTerrain(sender);
|
case "terrain-check" -> checkTerrain(sender);
|
||||||
case "terrain-unload" -> unloadTerrain(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()
|
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
||||||
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
||||||
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
||||||
@@ -66,6 +116,232 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
return true;
|
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 {
|
private void create(CommandSender sender) throws ReflectiveOperationException {
|
||||||
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
||||||
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
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);
|
int y = world.getHighestBlockYAt(x, z);
|
||||||
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
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, y - 3, z).getType() != Material.GREEN_CONCRETE
|
||||||
|
|| world.getBlockAt(x, world.getMinHeight(), z).getType() != Material.GREEN_CONCRETE
|
||||||
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
||||||
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
||||||
+ ": " + world.getBlockAt(x, y, z).getType());
|
+ ": " + world.getBlockAt(x, y, z).getType());
|
||||||
@@ -212,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean regionized() {
|
public boolean regionized() {
|
||||||
return false;
|
return J.isFolia();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean primaryThread() {
|
public boolean primaryThread() {
|
||||||
return Bukkit.isPrimaryThread();
|
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> runGlobal(Runnable task) {
|
public CompletableFuture<Void> runGlobal(Runnable task) {
|
||||||
@@ -229,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
result.completeExceptionally(failure);
|
result.completeExceptionally(failure);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (Bukkit.isPrimaryThread()) {
|
if (primaryThread()) {
|
||||||
wrapped.run();
|
wrapped.run();
|
||||||
} else {
|
} else if (!J.runGlobal(wrapped)) {
|
||||||
Bukkit.getScheduler().runTask(this, wrapped);
|
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
|
||||||
}
|
}
|
||||||
return result;
|
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"
|
"block": "minecraft:lime_wool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"rockPalette": {
|
"rockPalette": {
|
||||||
"palette": [
|
"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.
Loaded 100 of 1160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user