Compare commits

...
Author SHA1 Message Date
Brian Neumann-Fopiano 15bc52e9b5 V+ 2026-08-28 17:06:58 -04:00
Brian Neumann-Fopiano 9103796845 Toggle Vision entities
- Hide living-entity markers by default while retaining player markers.
- Add an Entities control beside Grid and stop polling hidden entity snapshots.
- Localize the new control and status messages across all bundled languages.
- God i gate these kinds of commit messages
2026-08-28 16:41:06 -04:00
Brian Neumann-Fopiano 27799039d1 Bind generation context
- Resolve first-generation engine binding from authoritative published level context.
- Validate generator identity and recover canonical Overworld binding from stale snapshots.
- Add shared loader regression tests and their Mockito test dependency.
2026-08-28 16:30:11 -04:00
Brian Neumann-Fopiano fd8c55076e Bridge host levels
- Publish and remove dynamic levels through host lifecycle methods when available.
- Attach hybrid host level data before engine binding and publication.
- Preserve native loader behavior when host extensions are absent.
2026-08-28 16:30:05 -04:00
Brian Neumann-Fopiano 95d8523d1d Route hybrid schedulers
- Select Bukkit scheduling only when the bound platform identifies as Bukkit.
- Route hybrid Youer hosts through the modded scheduler despite Bukkit classes on the classpath.
- Add regression coverage for Bukkit and NeoForge platform identities.
2026-08-28 16:29:58 -04:00
Brian Neumann-Fopiano ecec18509c Honor bound platform
- Route shared runtime hooks through the active Iris platform instead of classpath presence.
- Keep Bukkit-specific mantle and Matter behavior behind explicit platform capabilities.
- Cover hybrid classpaths and bound hook precedence with regression tests.
2026-08-28 16:29:53 -04:00
Brian Neumann-Fopiano 1ca943ab51 Remove Sentry
- Remove automatic error reporting and server-identifying attachments.
- Remove Sentry dependencies, release tooling, settings, and test commands.
- Keep failures operator-visible through local stack traces and logging.
2026-08-28 16:29:33 -04:00
Brian Neumann-Fopiano 1a561da2cd update gradl 2026-08-27 19:24:33 -04:00
Brian Neumann-Fopiano 2b92fbf825 iris release commits 2026-08-27 02:47:05 -04:00
Brian Neumann-Fopiano f398c55091 Remove Hydrology 2026-08-27 00:45:15 -04:00
Brian Neumann-Fopiano bdaa84975a f 2026-08-25 15:49:54 -04:00
Brian Neumann-Fopiano 07cc76614f Way too much 2026-08-25 12:52:12 -04:00
Brian Neumann-Fopiano 2797a5bf22 dwa 2026-08-24 16:50:57 -04:00
Brian Neumann-Fopiano 37b01b73c4 Imagemap sanitization 2026-08-24 13:44:44 -04:00
Brian Neumann-Fopiano e20bb86f40 f 2026-08-24 02:27:26 -04:00
Brian Neumann-Fopiano fe5651f854 d
d
2026-08-23 13:56:23 -04:00
Brian Neumann-Fopiano dce7af1955 object cleanup 2026-08-22 11:29:37 -04:00
Brian Neumann-Fopiano 770240e4f7 Merge pull request #1249 from VolmitSoftware/unification
Iris 4.0 unification: pure-JVM core + SPI with Bukkit/Fabric/Forge/NeoForge
adapters, art.arcane package layout, and the Gradle Groovy build.

Conflicts resolved in favour of unification for every path. The six commits
master gained after the fork point are all superseded there:
  - CraftEngine data provider (#1227): present, ported to CraftEngine 26.7.3
  - BlockProperty ofDouble/ofLong: present
  - 1.21.11 ambient-particle and ultrawarm dripstone datapack fixes: present
  - libs.versions.toml craftengine entries and the momirealms repo: present
  - build.gradle.kts / core/build.gradle.kts: replaced by the .gradle build

Merged tree is identical to origin/unification.
2026-08-21 23:30:51 -04:00
Robert Stoneking fe684083e7 Adding myself and fixing Windows Tests. 2026-08-21 20:59:26 -06:00
Brian Neumann-Fopiano 34092da6be d 2026-08-21 02:03:24 -04:00
Brian Neumann-Fopiano f43df6c232 d 2026-08-20 15:04:25 -04:00
Brian Neumann-Fopiano 8eaafa3c69 Object fixes / PS 2026-08-20 11:49:39 -04:00
Brian Neumann-Fopiano 5cacf6d435 MV 2026-08-20 10:59:17 -04:00
Brian Neumann-Fopiano fec655def4 ddd 2026-08-19 22:21:25 -04:00
Brian Neumann-Fopiano 41b2b53ac8 dwa 2026-08-19 19:36:03 -04:00
Brian Neumann-Fopiano 4adef4576f adding mroe of this 2026-08-18 11:52:33 -04:00
Brian Neumann-Fopiano 03746468ec COntent 2026-08-16 23:53:42 -04:00
Brian Neumann-Fopiano a1cef36f10 a 2026-08-15 23:02:50 -04:00
Brian Neumann-Fopiano 7691d2df53 dwa 2026-08-15 22:32:40 -04:00
Brian Neumann-Fopiano fc0fdf4ce4 d 2026-08-15 21:32:00 -04:00
Brian Neumann-Fopiano ef2e1b8f58 Modded adapters 2026-08-15 15:58:26 -04:00
Brian Neumann-Fopiano 8297e49ed2 Fix Iris replacement 2026-08-15 15:58:07 -04:00
Brian Neumann-Fopiano 4568b3fff1 chunk granularity 2026-08-15 15:58:00 -04:00
Brian Neumann-Fopiano 33638eadd7 height-bound samples 2026-08-15 15:57:54 -04:00
Brian Neumann-Fopiano a7baa1c1b9 nukkit artifacts 2026-08-15 15:57:46 -04:00
Brian Neumann-Fopiano d196ffe853 stabilize pregeneration 2026-08-15 15:57:36 -04:00
Brian Neumann-Fopiano 4535bf3eed probe 2026-08-15 15:57:30 -04:00
Brian Neumann-Fopiano 59f33bd921 probe- 2026-08-15 15:57:20 -04:00
Brian Neumann-Fopiano 57072d5bcd Correct Java 26.2 Iris world lifecycle 2026-08-15 11:27:46 -04:00
Brian Neumann-Fopiano 32beca236c ddd 2026-08-13 16:54:37 -04:00
Brian Neumann-Fopiano 1586b4bd1a dwa 2026-08-13 12:38:08 -04:00
Brian Neumann-Fopiano 14b6280668 Full audit fixpass: 93 defect fixes, 14 perf wins, vestigial cleanup 2026-08-13 01:01:01 -04:00
Brian Neumann-Fopiano 13e02ecd2f s 2026-08-12 16:31:27 -04:00
Brian Neumann-Fopiano c59592e448 rmd 2026-08-12 14:14:48 -04:00
Brian Neumann-Fopiano 365205ad0a d 2026-08-12 13:52:16 -04:00
Brian Neumann-Fopiano 12b97b7994 Fixes 2026-08-12 00:42:33 -04:00
Brian Neumann-Fopiano 8a189195ad d 2026-08-11 20:42:37 -04:00
Brian Neumann-Fopiano d9860782ef oops 2026-08-11 20:41:41 -04:00
Brian Neumann-Fopiano 623a02025c content 2026-08-11 20:35:27 -04:00
Brian Neumann-Fopiano 82290a3090 fix 2026-08-11 14:54:53 -04:00
Brian Neumann-Fopiano dfae45663a Fixes 2026-08-10 17:32:44 -04:00
Brian Neumann-Fopiano 998a5c9f5f dwa
(removed ai file from when i generated docs)
2026-08-10 15:48:22 -04:00
Brian Neumann-Fopiano ebfe278b3b Docvks 2026-08-10 15:47:26 -04:00
Brian Neumann-Fopiano 506787f51a Updated Docs, and Cortections 2026-08-08 00:29:48 -06:00
Brian Neumann-Fopiano c40e1cf152 Scary CI Builds. 2026-08-07 16:39:34 -06:00
Brian Neumann-Fopiano 68b842d5dc First Test MV 2026-08-07 14:04:17 -06:00
Brian Neumann-Fopiano 42b8db4856 BS 2026-08-06 16:23:56 -06:00
Brian Neumann-Fopiano dda5ea397e ADA 2026-08-06 07:41:58 -06:00
Brian Neumann-Fopiano 967c082372 d 2026-08-05 23:39:54 -06:00
Brian Neumann-Fopiano ce8c4ff578 d
d
2026-08-05 15:45:08 -06:00
Brian Neumann-Fopiano 70d355621c Temp vag 2026-08-05 14:30:07 -06:00
Brian Neumann-Fopiano a66cba6418 dwa 2026-08-05 02:28:14 -06:00
Brian Neumann-Fopiano cd217c05f9 Vwop 2026-08-05 01:16:30 -06:00
Brian Neumann-Fopiano aaccbacf32 dwa 2026-08-04 11:13:16 -06:00
Brian Neumann-Fopiano 10b2363226 ada 2026-08-02 04:16:06 -04:00
Brian Neumann-Fopiano a8217b42e9 F 2026-08-01 22:39:09 -04:00
Brian Neumann-Fopiano 324cf9e095 🧹 2026-07-30 12:31:39 -04:00
Brian Neumann-Fopiano 0afbfcb7a2 Changes 2026-07-30 02:36:15 -04:00
Brian Neumann-Fopiano 7ad4909518 Fix Iris entity spawn delegation 2026-07-29 18:10:55 -04:00
Brian Neumann-Fopiano 74741ac83e d 2026-07-27 13:46:24 -05:00
Brian Neumann-Fopiano d5a55ccfcf API 2026-07-26 12:12:49 -05:00
Brian Neumann-Fopiano de25b7d7ab d 2026-07-22 21:29:24 -05:00
Brian Neumann-Fopiano 9e1e202f15 dwa 2026-07-22 14:56:50 -05:00
Brian Neumann-Fopiano f6f06c1ab9 d 2026-07-21 12:00:17 -04:00
Brian Neumann-Fopiano d76dadec30 🔥 2026-07-18 21:48:52 -04:00
Brian Neumann-Fopiano 1d194e880b f 2026-07-18 19:16:14 -04:00
Brian Neumann-Fopiano ad41b6795c f 2026-07-18 18:12:04 -04:00
Brian Neumann-Fopiano 0193f75daa Fixes 2026-07-16 19:38:53 -04:00
Brian Neumann-Fopiano e8dd4e1127 iggy 2026-07-16 18:18:23 -04:00
Brian Neumann-Fopiano 699247b1de Vwoop 2026-07-16 12:26:01 -04:00
Brian Neumann-Fopiano 55b7460b9a s 2026-07-14 11:44:00 -04:00
Brian Neumann-Fopiano 45230c0689 dwa 2026-07-13 21:33:20 -04:00
Brian Neumann-Fopiano 4dea984d77 O-o .o-o. o-O 2026-07-12 20:53:51 -04:00
Brian Neumann-Fopiano cd05fd1bca World Changes 2026-07-12 17:06:30 -04:00
Brian Neumann-Fopiano e42ab6fae6 dswa 2026-07-11 19:28:40 -04:00
Brian Neumann-Fopiano 402bcb04fe dd 2026-07-10 04:05:24 -04:00
Brian Neumann-Fopiano 6111b5670a f 2026-07-09 18:34:16 -04:00
Brian Neumann-Fopiano 0a7a531646 Dont ask 2026-07-09 13:54:59 -04:00
Brian Neumann-Fopiano d1fec88769 d 2026-07-08 17:55:14 -04:00
Brian Neumann-Fopiano 848e056bb8 b 2026-07-05 10:16:15 -04:00
Brian Neumann-Fopiano 6b64624233 Fabric fix color my b 2026-07-04 17:42:50 -04:00
Brian Neumann-Fopiano d1cc83a40b fixes 2026-07-04 10:35:34 -04:00
Brian Neumann-Fopiano 927c48f8c7 Fixes 2026-07-03 21:37:08 -04:00
Brian Neumann-Fopiano 29ff3f01a7 Lovely 2026-07-03 17:25:58 -04:00
Brian Neumann-Fopiano a8e46e3590 iggy 2026-07-03 15:10:16 -04:00
Brian Neumann-Fopiano 75e8c22268 ff 2026-06-26 14:01:20 -04:00
Brian Neumann-Fopiano f777ea6b18 few more changes 2026-06-17 19:18:44 -04:00
Brian Neumann-Fopiano 8fb627f9c1 changes :) 2026-06-16 01:35:25 -04:00
Brian Neumann-Fopiano 3cfbcbf8ed Perf pass 2 2026-06-15 13:12:18 -04:00
Brian Neumann-Fopiano 79e80bef33 bump1 2026-06-14 13:52:57 -04:00
Brian Neumann-Fopiano 796d81e58b d 2026-06-14 01:21:11 -04:00
Brian Neumann-Fopiano b0a5d97e9c d 2026-06-13 22:03:58 -04:00
Brian Neumann-Fopiano ad6732812e d
d
2026-06-13 17:07:27 -04:00
Aidan Aeternum 28c4941941 v+ 2026-06-12 21:10:21 -04:00
Aidan Aeternum d452f10fd9 Merge pull request #1241 from VolmitSoftware/dev
3.9.2
2026-06-12 21:09:46 -04:00
Brian Neumann-Fopiano f482b8ef7e laDEDA 2026-06-12 15:52:01 -04:00
Brian Neumann-Fopiano e2f6061f98 Patches 2026-06-12 06:58:32 -04:00
Brian Neumann-Fopiano b98a341dc7 Cleanup 2026-06-12 02:54:34 -04:00
Brian Neumann-Fopiano 261a850af4 Remove Old Gource
(i can just reinstall it later)
2026-06-12 02:48:59 -04:00
Brian Neumann-Fopiano 516537db72 Forge Adapter 2026-06-12 02:48:22 -04:00
Brian Neumann-Fopiano 921a98f210 NeoForge Adapter 2026-06-12 02:48:14 -04:00
Brian Neumann-Fopiano 2bac948edb Fabric Parity Worldgen 2026-06-12 02:48:05 -04:00
Brian Neumann-Fopiano 26d9115904 Fabric Tooling 2026-06-12 02:47:54 -04:00
Brian Neumann-Fopiano 763ff90a61 Headless Pipeline 2026-06-12 02:47:42 -04:00
Brian Neumann-Fopiano f32094a404 Headless Generation 2026-06-12 02:47:32 -04:00
Brian Neumann-Fopiano 6b24105640 World Manager Seam 2026-06-12 02:47:20 -04:00
Brian Neumann-Fopiano a3d7269a99 Headless Classload 2026-06-12 02:47:10 -04:00
Brian Neumann-Fopiano e1196b0364 Vector Currency 2026-06-12 02:47:02 -04:00
Brian Neumann-Fopiano 4c462ba78a Resolution Currency 2026-06-12 02:46:46 -04:00
Brian Neumann-Fopiano 65d4abe5c0 Forge Skeleton 2026-06-12 02:46:34 -04:00
Brian Neumann-Fopiano 5142c048c7 Plugin Eviction 2026-06-12 02:46:22 -04:00
Brian Neumann-Fopiano 88c87175b3 Eviction Seams 2026-06-12 02:46:13 -04:00
Brian Neumann-Fopiano 56a0c75a39 Host Bridge 2026-06-12 02:46:13 -04:00
Brian Neumann-Fopiano 704c94aab4 Periphery Facades 2026-06-12 02:45:37 -04:00
Brian Neumann-Fopiano b5d08b6203 Service Facade 2026-06-12 02:45:18 -04:00
Brian Neumann-Fopiano b584cb4193 Logging Facade 2026-06-12 02:45:06 -04:00
Brian Neumann-Fopiano af3035088a Sealed Seam 2026-06-12 02:44:54 -04:00
Brian Neumann-Fopiano df888e5516 Entity Seam 2026-06-12 02:44:42 -04:00
Brian Neumann-Fopiano ca10edef0c Biome Writer 2026-06-12 02:44:29 -04:00
Brian Neumann-Fopiano 986c7f12cd Structure Hooks 2026-06-12 02:44:18 -04:00
Brian Neumann-Fopiano 6c5e991051 Engine Tier 3 2026-06-12 02:44:05 -04:00
Brian Neumann-Fopiano 1a834ed6f6 Serial Gate 2026-06-12 02:43:45 -04:00
Brian Neumann-Fopiano c9929e0880 Engine Slimming 2026-06-12 02:43:30 -04:00
Brian Neumann-Fopiano 3063801a3a Purity Ratchet 2026-06-12 02:43:15 -04:00
Brian Neumann-Fopiano 13002b04df World Ops 2026-06-12 02:43:02 -04:00
Brian Neumann-Fopiano 1c9f9d92bb Neutral Pipeline 2026-06-12 02:42:26 -04:00
Brian Neumann-Fopiano 4568f0b4b6 Currency Seams 2026-06-12 02:42:13 -04:00
Brian Neumann-Fopiano 1c08eb23b2 Bukkit SPI 2026-06-12 02:41:47 -04:00
Brian Neumann-Fopiano 7e224325f3 Restructure into cross-platform monorepo: spi module, adapters tree, buildAll 2026-06-12 02:41:06 -04:00
Brian Neumann-Fopiano a1ff3b2c55 Merge remote-tracking branch 'origin/unification' into unification 2026-06-11 12:42:57 -04:00
Brian Neumann-Fopiano abfc122171 SMID 2026-06-11 12:42:47 -04:00
Brian Neumann-Fopiano 6a68884ce9 Make world generation fully deterministic; add golden-hash verification harness
Generation now produces bit-identical output across repeated runs, server
restarts, and parallel generation (golden-hash verified, 289/289 chunks).

Engine fixes:
- MatterGenerator: components flagged complete before generateLayer finished
  (raiseFlagUnchecked), letting concurrent neighbors consume half-written
  mantle data; switched to completion-locked raiseFlagSuspend
- MantleObjectComponent: procedural objects (trees/ruins/formations/coral/
  fungi/crystals) were absent from computeRadius, so canopies crossed chunk
  borders without ordering guarantees; variant footprints now counted
- MantleCarvingComponent: carve profile ordering depended on IdentityHashMap
  iteration and hashCode sort ties; replaced with first-seen column-scan
  order and creation-sequence tiebreaks
- IrisObject/IrisPostModifier: shared BlockData instances mutated in place
  (vine facing x3, auto-waterlog x2, leaf persistence); clone before mutate
- IrisObject: slope-following placement mutated the shared pack-defined
  rotation config; now builds a local rotation on a copied placement
- IrisObjectPlacement: surface-warp CNG seeded from first-caller chunk RNG;
  now seeded from the engine seed manager
- GenerationCacheWarmer: engine init pre-bakes placement/decorator/ore/
  procedural caches in sorted order, removing lazy first-touch ordering
- IrisProceduralPlacement: expose getVariantObjects for radius computation

Verification harness:
- /iris dev goldenhash: buffered chunk generation (no world writes), SHA-256
  per-chunk block+biome digests, golden capture/verify with per-chunk
  mismatch reporting, auto-diagnosis (repeat-gen + mantle-reset comparison,
  block-level diffs), deep per-chunk blockstate dumps, threads/deep params,
  full mantle reset (saveAll + tectonic file wipe) for reproducible runs
- IrisEngineSVC: mantle trim/unload suspended while a goldenhash scan runs
- MantleCarvingComponentTop2BlendTest: sequence-tiebreak regression test
2026-06-11 12:40:21 -04:00
Brian Neumann-Fopiano c5a2a57822 SMID 2026-06-10 23:42:12 -04:00
Brian Neumann-Fopiano 58d24ca55e dwa 2026-06-10 21:21:18 -04:00
Brian Neumann-Fopiano c6c02a9040 f 2026-06-10 13:52:09 -04:00
Brian Neumann-Fopiano a530a03459 nms patches 2026-06-10 09:00:23 -04:00
Brian Neumann-Fopiano a82b8f4a9c dwa 2026-06-09 20:14:27 -04:00
Brian Neumann-Fopiano cf2e7df298 c 2026-06-08 00:20:23 -04:00
Brian Neumann-Fopiano d20b3af9b9 addons 2026-06-07 15:23:41 -04:00
Brian Neumann-Fopiano 01d98d2141 Procedurality 2026-06-07 01:55:17 -04:00
Brian Neumann-Fopiano febe930db0 dwa 2026-06-07 00:04:20 -04:00
Brian Neumann-Fopiano 5c832ba128 ignore 2026-06-06 18:39:57 -04:00
Brian Neumann-Fopiano 03f9e233da No more. memory leak in pregen 2026-06-03 19:08:32 -04:00
Brian Neumann-Fopiano 11efae428f x 2026-06-03 01:21:14 -04:00
Brian Neumann-Fopiano 1a6fff35e8 fff 2026-05-31 19:40:17 -04:00
Brian Neumann-Fopiano 540a416bc9 Good points 2026-05-31 07:51:24 -04:00
Brian Neumann-Fopiano 7eb6e87a81 Perf Pass 1 2026-05-30 22:39:51 -04:00
Brian Neumann-Fopiano 9ff1c502ef dwa 2026-05-30 01:27:53 -04:00
Brian Neumann-Fopiano 08c6f7ddf9 lmao 2026-05-29 23:52:58 -04:00
Brian Neumann-Fopiano 4bc1913f03 redesign for Structures 2026-05-29 19:54:10 -04:00
Brian Neumann-Fopiano 821cc027db fix 2026-04-23 22:44:52 -04:00
Brian Neumann-Fopiano 1968ef2f2c Wip floaters 2026-04-23 21:29:23 -04:00
Brian Neumann-Fopiano ce29b70618 Content 2026-04-23 16:38:32 -04:00
Brian Neumann-Fopiano 3d128b70a7 RedoDeco 2026-04-22 19:23:50 -04:00
Brian Neumann-Fopiano 23fad24fb7 Whoops 2026-04-22 16:33:25 -04:00
Brian Neumann-Fopiano 6df718e6ca floatt 2026-04-22 09:42:45 -04:00
Brian Neumann-Fopiano 787c728060 Amending / removing all structures will redesign later im over this 2026-04-20 18:20:53 -04:00
Brian Neumann-Fopiano 88bbce82fe Object Studio and Plausibilizer 1.0 2026-04-18 14:50:36 -04:00
Brian Neumann-Fopiano e6a8351e57 Bulk changes and fixes
namely the chunk issue, and objects not wantingto place on cave tops.
2026-04-17 22:28:35 -04:00
Brian Neumann-Fopiano b82472d521 Loads 2026-04-17 16:14:49 -04:00
Brian Neumann-Fopiano 8c05f1bf1d polish 2026-04-15 16:35:49 -04:00
Brian Neumann-Fopiano 6a5616abae d 2026-04-15 15:52:18 -04:00
Brian Neumann-Fopiano 6096c39192 OptiPass3 2026-04-15 15:13:35 -04:00
Brian Neumann-Fopiano b42d0e4710 OptiPass 2
Smart Sampling and stuff
2026-04-15 13:26:52 -04:00
Brian Neumann-Fopiano dcb3306197 Opti pass 1 2026-04-15 12:50:12 -04:00
Brian Neumann-Fopiano 9a231b2bcf Studio FIxes 2026-04-15 09:45:02 -04:00
Brian Neumann-Fopiano aa706d027b WIP Building for latest.
Still Quantizing, WOrking on that next
also removed prebake
2026-04-15 09:05:41 -04:00
Brian Neumann-Fopiano 568fb07f66 gr 2026-04-13 17:06:29 -04:00
Julian Krings e7d89cadd8 fix compile 2026-04-12 11:58:33 +02:00
Julian Krings cddd6b16d6 add CraftEngine data provider (#1227) 2026-04-12 11:55:36 +02:00
Julian Krings b2ac4e7f93 fix invalid json for ultrawarm dimensions on 1.21.11 2026-04-12 11:55:36 +02:00
Julian Krings c2bb32459f fix ambient particles breaking datapack generation on 1.21.11 2026-04-12 11:55:35 +02:00
Brian Neumann-Fopiano bf207b7062 speed pass 2026-02-24 03:57:07 -05:00
Brian Neumann-Fopiano 18d4dce1db dwa 2026-02-22 23:25:01 -05:00
Brian Neumann-Fopiano 651dfa247e perfpass 2026-02-22 09:58:33 -05:00
Brian Neumann-Fopiano 5b1ff6d2f7 ugh 2026-02-22 09:08:31 -05:00
Brian Neumann-Fopiano 589baafc28 kts gone 2026-02-22 08:59:50 -05:00
Brian Neumann-Fopiano 130073989d Fix Shafts 2026-02-22 07:09:59 -05:00
Brian Neumann-Fopiano 2ca8cc7ad3 f 2026-02-21 18:56:10 -05:00
Brian Neumann-Fopiano 9f35855599 dwa 2026-02-21 04:14:18 -05:00
Brian Neumann-Fopiano 5110a33857 forgor 2026-02-21 03:50:43 -05:00
Brian Neumann-Fopiano a38e87cce3 f 2026-02-21 03:49:10 -05:00
Brian Neumann-Fopiano 1d09cd5f0f dwa 2026-02-21 03:37:44 -05:00
Brian Neumann-Fopiano 72c891ce5b dwa 2026-02-20 23:17:17 -05:00
Brian Neumann-Fopiano 3964185a81 Working 2026-02-20 17:53:11 -05:00
Brian Neumann-Fopiano afc5f67bc2 f 2026-02-19 22:35:29 -05:00
Brian Neumann-Fopiano 1c72d6410f f 2026-02-19 19:50:29 -05:00
Brian Neumann-Fopiano c2c612eb35 d 2026-02-19 16:49:54 -05:00
Brian Neumann-Fopiano 415c2f5837 JIGGY! 2026-02-19 04:09:06 -05:00
Brian Neumann-Fopiano b491d9efd0 im not getting Jiggy with it 2026-02-19 02:02:31 -05:00
Brian Neumann-Fopiano d643461e2e f 2026-02-19 01:23:08 -05:00
Brian Neumann-Fopiano 1f41e195cf p1 2026-02-18 23:44:23 -05:00
Brian Neumann-Fopiano 7b9280083f f 2026-02-18 23:43:02 -05:00
Brian Neumann-Fopiano 73525c170d Noise Pass 2026-02-18 23:10:49 -05:00
Brian Neumann-Fopiano 16c861a781 f 2026-02-18 20:56:30 -05:00
Brian Neumann-Fopiano d6bd5ec82f d 2026-02-18 20:24:25 -05:00
Brian Neumann-Fopiano 1829fa3eb1 d
d
2026-02-18 15:18:21 -05:00
Brian Neumann-Fopiano 8d215e3911 V 2026-02-18 15:08:55 -05:00
Brian Neumann-Fopiano ba819e4899 canvas checks 2026-02-17 22:44:01 -05:00
Brian Neumann-Fopiano 05d79b6d40 Create std 2026-02-17 06:18:55 -05:00
Brian Neumann-Fopiano cf64fbb730 Waow 2026-02-17 02:54:34 -05:00
Brian Neumann-Fopiano 43a65135b1 WIP - Dont compile 2026-02-17 00:34:34 -05:00
Brian Neumann-Fopiano 440c1bec6c "Folia" 2026-02-16 06:07:25 -05:00
Brian Neumann-Fopiano 0fd8bcc488 f 2026-02-15 17:43:28 -05:00
Brian Neumann-Fopiano bb1c0b8f68 FIXES 2026-02-15 04:49:23 -05:00
Brian Neumann-Fopiano fc25acdea2 CMD 2026-02-14 23:55:01 -05:00
Brian Neumann-Fopiano 0aa6f6d523 d 2026-02-14 19:58:45 -05:00
Brian Neumann-Fopiano a79c1a7954 ignore, just tracking info 2026-02-14 18:11:43 -05:00
Brian Neumann-Fopiano 7885762cd7 shakey shakey 2026-02-14 05:44:10 -05:00
Brian Neumann-Fopiano 86ed3f0095 OOPS
kinda need this
2026-02-14 04:52:30 -05:00
Brian Neumann-Fopiano 99fcc8fd03 WIP 2026-02-14 01:56:05 -05:00
Brian Neumann-Fopiano 41888e33f6 Fixing fallbacks for macos Folk
Compiles fine, just stricter defaults
2026-02-11 00:01:23 -05:00
Aidan Aeternum 25fa2553e5 v+ 2026-02-02 03:28:49 -05:00
Aidan Aeternum 86f78baecf Merge pull request #1236 from VolmitSoftware/dev
3.9.1
2026-02-02 03:28:30 -05:00
Julian Krings c31158578f fix datapack generation on 1.21.11 2026-01-29 16:40:33 +01:00
Aidan Aeternum 4e86d7d634 v+ 2026-01-27 20:09:25 -05:00
Aidan Aeternum 62fe29cf34 Merge pull request #1235 from VolmitSoftware/dev
3.9.0
2026-01-27 20:08:36 -05:00
Julian Krings a3bcea4f3e increase memory assumption per region 2026-01-26 21:37:29 +01:00
Julian Krings 7befce1084 initial 1.21.11 support 2026-01-23 11:46:15 +01:00
Aidan Aeternum 351a1aa495 v+ 2026-01-15 02:50:30 -05:00
Aidan Aeternum d2cb8a9032 Merge pull request #1234 from VolmitSoftware/dev
3.8.2
2026-01-15 02:50:03 -05:00
Julian Krings 509715087c fix typo 2026-01-13 15:11:21 +01:00
Julian Krings 12527ecdd8 improve cachedChunks lookup in the mantle writer 2026-01-05 14:58:58 +01:00
Julian Krings 01a2999e03 decrease locking for mantle generation 2026-01-05 14:58:09 +01:00
Julian Krings dbafe84fa5 make context dereference method fail-safe 2026-01-02 14:12:44 +01:00
Julian Krings 40b020fc5d improve matter generator once again 2026-01-02 14:10:35 +01:00
Julian Krings d15f7d62d1 cleanup async world ticker 2026-01-02 14:10:35 +01:00
Julian Krings f9c062c794 improve mantle cleanup to prevent thread starvation 2026-01-02 14:10:34 +01:00
Aidan Aeternum 6a89b8bd06 v+ 2026-01-02 06:17:00 -05:00
Aidan Aeternum 194fcb2ea7 Merge pull request #1232 from VolmitSoftware/dev
3.8.1
2026-01-02 06:16:28 -05:00
Julian Krings 3b68f855b2 get back some speed by loading four mantle regions into cache at once 2025-12-31 13:38:42 +01:00
Julian Krings cfbf68d37a add check for headless environments to the edit command 2025-12-31 13:01:18 +01:00
Julian Krings 9999f3e429 fix chunk artifacts 2025-12-31 13:00:45 +01:00
Julian Krings 97fa0562a4 fix custom block data 2025-12-23 22:51:41 +01:00
Julian Krings 3abe671851 suppress unchecked cast warning for the ChunkedDataCache 2025-12-18 18:53:59 +01:00
Julian Krings e164a3bb5c Merge branch 'feat/safeguard' into dev
# Conflicts:
#	build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-12-18 18:44:36 +01:00
Julian Krings 4dfb4441e4 fix buildscript 2025-12-18 14:23:51 +01:00
Julian Krings e686f67453 add command to fix offsets of all objects 2025-12-18 14:23:35 +01:00
Julian Krings e2eed4812a ignore empty objects for shrinkwrap 2025-12-18 14:22:40 +01:00
Julian Krings 1737833bfe fix legacy object parsing 2025-12-18 14:22:21 +01:00
piexel a4c5f46c37 Fix /iris object convert for .schem files with 128+ block types 2025-12-18 11:25:38 +01:00
piexel 025fa833c4 Fix Grass won't place on moss blocks 2025-12-18 11:21:16 +01:00
Julian Krings 98cc82cc3d Fix jigsaw editor bricking when opening a piece a missing object 2025-12-18 11:13:43 +01:00
Julian Krings 298365f588 fix studio open not working when not in spectator 2025-12-18 11:06:09 +01:00
Julian Krings 90e5720e2e improve buildscript 2025-12-18 11:05:28 +01:00
Julian Krings 7cd43791f4 fix offset with shrinkwrap 2025-12-18 11:05:14 +01:00
Julian Krings a251d192ad fix loot for objects underground 2025-12-18 11:00:04 +01:00
Julian Krings abfff28f43 fix redstone not rotating 2025-12-18 10:59:57 +01:00
Julian Krings fbdae4c928 improve overwriting of the regenerated chunks 2025-12-18 10:59:32 +01:00
Aidan Aeternum 93ca26e368 v+ 2025-12-17 12:12:25 -05:00
Aidan Aeternum 8c1db1c223 Merge pull request #1231 from VolmitSoftware/dev
3.8.0
2025-12-17 12:11:28 -05:00
Julian Krings 123708601f improve pregen cache 2025-12-01 16:01:54 +01:00
Julian Krings b5ab4968ba fix compile 2025-11-30 15:36:36 +01:00
Julian Krings 93ae6037bd whoops forgot to disable craftbukkit relocations on <=1.20.4 2025-11-29 11:49:15 +01:00
Julian Krings dd8e487a3b fix tile entity serialization on paper 1.21.6+ servers 2025-11-29 11:13:30 +01:00
Julian Krings d3c8377a12 allow switching between buildtools and userdev 2025-11-29 11:12:16 +01:00
Julian Krings 84f3687c0c update compat for iron bars 2025-11-27 16:09:31 +01:00
Julian Krings ed1e1f1181 whoops forgot those providers 2025-11-26 21:33:03 +01:00
Julian Krings 528c97f367 replace IrisCustomData class with a proxy to keep full access to the base BlockData 2025-11-26 21:32:13 +01:00
Julian Krings f7a459f3bc initial 1.21.9/10 bindings 2025-11-26 21:25:05 +01:00
Julian Krings cd179b4321 reintroduce native threads for the updater as a setting and cleanup 2025-11-26 20:55:48 +01:00
Julian Krings 6373dbb1b8 potential optimization for the noise cache 2025-11-21 16:21:54 +01:00
Julian Krings 0b0797f876 minor optimization for chunk updater 2025-11-21 16:21:31 +01:00
Julian Krings 446acefc91 minor optimization for the mantle writer 2025-11-21 16:21:02 +01:00
Julian Krings 7a44e555b2 rename noise cache size setting and decrease default value 2025-11-21 16:20:32 +01:00
Julian Krings 57d4c2935c add cache sizes to engine status 2025-11-21 16:16:15 +01:00
Julian Krings 234fb1b0c4 also include parent class for schema generation 2025-11-08 00:58:12 +01:00
Julian Krings 0882b5acc4 use parent shared classloader to reflect intelij behavior 2025-11-06 22:32:28 +01:00
Julian Krings 18da26e1fa minor performance improvement 2025-11-06 22:27:14 +01:00
Julian Krings 65aa95f2a5 fix wrong radius when marking mantle chunks as completed 2025-11-06 22:25:37 +01:00
Julian Krings 5330ddc4ec fix deleting object ids with mantle cleanup 2025-11-06 22:20:30 +01:00
Julian Krings a7fdd37569 fix studio loot command 2025-11-06 22:16:27 +01:00
Julian Krings a226fea9e2 fix regen command 2025-11-06 16:22:02 +01:00
Julian Krings 8cea165a29 even more performance improvements 2025-11-06 15:29:03 +01:00
Julian Krings 1d7cba184c fix linear palette not growing correctly 2025-11-06 14:41:39 +01:00
Julian Krings 4ca7ea3911 minor speed improvements 2025-11-01 22:36:25 +01:00
Julian Krings ea5919def2 stop writing first access each time the engine is opened 2025-10-30 16:40:35 +01:00
Julian Krings be35e49302 use coroutines for mantle generation 2025-10-30 16:40:08 +01:00
Julian Krings aadd03990a optimize data palette for mantle slices 2025-10-27 19:53:47 +01:00
Julian Krings 38a579453d optimize noise cache 2025-10-26 13:49:03 +01:00
Julian Krings 0bf5da2ca1 optimize object maps 2025-10-26 13:48:11 +01:00
Julian Krings 317848692e improve regen speed 2025-10-06 14:12:01 +02:00
Julian Krings 22118de9e9 fix object smart bore 2025-10-06 14:10:37 +02:00
Julian Krings d7039d120b fix place commands causing unwanted block updates 2025-10-06 13:00:00 +02:00
Julian Krings 979ee4e7d8 switch hashing algorithm for objects once more 2025-10-05 21:46:12 +02:00
Julian Krings f68d45bd30 dynamically resolve snippet classes 2025-10-05 00:21:38 +02:00
Julian Krings b86d7f303e restructure the shared kts classloader to be more consistent 2025-10-05 00:20:55 +02:00
Julian Krings c573843314 fix kts dependency resolver 2025-10-05 00:17:57 +02:00
Julian Krings 51a7bef18e whoops forgot escaping the path 2025-10-04 17:19:11 +02:00
Julian Krings 0e237aa1ad fix generated build.gradle.kts on external dives on windows 2025-10-04 13:41:48 +02:00
Julian Krings b46c413f6b make gradle setup print to console on failure 2025-10-04 13:40:47 +02:00
Julian Krings f3ef1ca2ae fix typo in preprocessors description 2025-10-04 13:40:07 +02:00
Julian Krings 703e61dd54 fix preprocessors not applying reliably 2025-10-04 13:39:33 +02:00
Aidan Aeternum e1ec6b7827 v+ 2025-10-03 16:08:05 -04:00
Aidan Aeternum f94292fdac Merge pull request #1228 from VolmitSoftware/dev
3.7.11
2025-10-03 16:07:47 -04:00
Julian Krings 7d153bf985 optimize objects to avoid hash collision 2025-10-02 15:25:37 +02:00
Julian Krings f85f15ed02 fix converter for complex schematic 2025-10-02 11:43:16 +02:00
Julian Krings 867686eced fix deserialisation of unversioned mantle plates 2025-10-01 12:57:38 +02:00
Julian Krings 526efd3ae1 restructure safeguard 2025-09-29 15:45:56 +02:00
Aidan Aeternum 9d796bd2a0 v+ 2025-09-27 07:34:58 -04:00
Aidan Aeternum 1a9a5d80ad Merge pull request #1223 from VolmitSoftware/dev
3.7.10
2025-09-27 07:34:31 -04:00
Julian Krings c5c7f9bdc5 fix minor issue with nms tools 2025-09-22 18:10:34 +02:00
Julian Krings 01a421b732 add file extensions to the script property descriptions 2025-09-21 22:35:34 +02:00
Julian Krings ae92bcf194 add kts hook for chunk updates 2025-09-21 22:30:48 +02:00
Julian Krings 7e7933858b suppress json syntax exceptions from being reported to sentry 2025-09-21 15:47:29 +02:00
Julian Krings 9c073ecbcb fix infinite loop due to writing the gradle.kts 2025-09-21 14:01:59 +02:00
Julian Krings f4617c1996 add tabcompletion for mythic mobs mob stacks 2025-09-21 12:51:59 +02:00
Julian Krings 21a2e4feef fix hotloading when changing kts 2025-09-21 12:13:57 +02:00
Julian Krings 258d0d3aaa make sure that the pack is installed correctly 2025-09-21 00:15:34 +02:00
Julian Krings 27b2fd0823 show other packs data again 2025-09-20 23:31:33 +02:00
Julian Krings 0524adb0df fix compile 2025-09-20 23:05:24 +02:00
Julian Krings 3981b0976d cleanup command framework and fix random locator fails 2025-09-20 23:05:16 +02:00
Julian Krings b5811cae08 cleanup IrisData usage 2025-09-20 23:04:11 +02:00
Julian Krings c998fd1fd9 move additional build data in its own constants class 2025-09-20 17:25:20 +02:00
Julian Krings a7d874d37f use scheduled thread pool for scoreboard svc to prevent freezes 2025-09-20 15:27:49 +02:00
Julian Krings 7c41f86fb3 add slope condition for slabs 2025-09-20 14:28:33 +02:00
Julian Krings e5e0561d5a cleanup world creator 2025-09-17 18:02:45 +02:00
Julian Krings d50cdfec3e cleanup pack installation 2025-09-17 18:02:20 +02:00
Julian Krings 00997c1902 return if missing datapack entries were found 2025-09-17 17:36:18 +02:00
Julian Krings 3095a92522 add experimental setting to force place custom blocks as early as possible 2025-09-10 23:06:46 +02:00
Julian Krings fca309dec7 move the loading block data message into debug 2025-09-10 17:02:51 +02:00
Julian Krings 2793ed1035 send block update after placing itemsadder blocks 2025-09-10 17:01:29 +02:00
Julian Krings e5908285af remove old worlds from cache before retrieving 2025-09-10 16:15:01 +02:00
Julian Krings a8ee321eb8 Allow placing datapack structures in Iris worlds (#1225)
* Allow placing datapack structures in Iris worlds

* command for generating configs for datapack structures

* remove the sub dir from the snippet key
2025-09-07 18:36:08 +02:00
Julian Krings aa14242b54 another mantle fix 2025-09-07 16:50:23 +02:00
Julian Krings f6968269b4 add toggle to offset noise types fixing seeds 2025-09-06 14:35:25 +02:00
Julian Krings 0d0251e2f1 add toggle to change rarity algorithm 2025-09-06 14:21:52 +02:00
Julian Krings 81b8fb02ae replace rarity calculation due to it breaking at more than two values 2025-09-06 14:21:52 +02:00
Julian Krings 77842489e5 add command for checking the effective rarity of regions 2025-09-06 14:21:52 +02:00
Julian Krings eda1f59d3a fix broken cave floors 2025-09-05 17:02:36 +02:00
Julian Krings 5418868559 fix hotloading mantle components 2025-09-05 16:56:56 +02:00
Julian Krings 1d81daafbb add option to control the noise threshold for cave shapes 2025-09-05 16:06:53 +02:00
Julian Krings 609a3585c1 use flat maven repository for classpath context 2025-09-05 16:04:49 +02:00
Julian Krings 571dde608c add shared class loader support for script dependencies 2025-09-05 12:21:28 +02:00
Julian Krings 3a13f5a7c1 fix dependency resolve failing on new intelij versions 2025-09-04 23:07:37 +02:00
Julian Krings e5654b74d4 Auto Completion for block properties (#1222) 2025-09-04 18:06:00 +02:00
Julian Krings a75738dd7a minor noise optimizations 2025-09-04 16:13:41 +02:00
Julian Krings 003be1f88b use mantle chunk for deposit modifier 2025-09-04 13:56:05 +02:00
Julian Krings 1eaafae20d move caffeine cache executors into a virtual thread executor 2025-09-04 13:55:34 +02:00
Julian Krings 79088c0305 fix hyperlock not releasing on exceptions 2025-09-04 13:55:03 +02:00
Julian Krings 9e147774bd revert mantle change to hopefully fix deadlock 2025-09-04 13:54:28 +02:00
Julian Krings e51b632c8f Merge pull request #1221 from VolmitSoftware/feat/kts
Kotlin script engine
2025-09-04 13:43:08 +02:00
Julian Krings 1aa64c9a02 add hook for custom engine modes 2025-09-03 17:02:39 +02:00
Julian Krings 1e148d8fcd minor code cleanup 2025-09-03 13:32:08 +02:00
Julian Krings f63cabd8b8 fix mantle flag serialization 2025-09-03 12:56:02 +02:00
Julian Krings 176d3a5f9f remove old system property 2025-09-02 22:14:59 +02:00
Julian Krings eb184983de fix json property description on vscode 2025-09-02 22:09:32 +02:00
Julian Krings d1c307865d properly generate gradle wrapper for kts tab completion 2025-09-02 22:05:11 +02:00
Julian Krings 4a26b8b34f fix compile 2025-09-02 21:57:34 +02:00
Julian Krings 33fd01c3ac Merge branch 'dev' into feat/kts
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
#	core/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java
#	core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java
#	core/src/main/java/com/volmit/iris/util/mantle/Mantle.java
#	core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java
2025-09-02 12:47:23 +02:00
Julian Krings 34874080e7 cleanup some array types for json schema generation 2025-09-02 12:28:54 +02:00
Julian Krings 43131ed8f6 make jar scanner also load child classes 2025-09-02 12:27:26 +02:00
Julian Krings 9ea425aee4 isolate io stuff in its own MultiBurst 2025-09-02 12:27:01 +02:00
Aidan Aeternum 709f05c1a5 v+ 2025-08-29 18:23:28 -04:00
Aidan Aeternum ec74add5de Merge pull request #1219 from VolmitSoftware/dev
3.7.2
2025-08-29 18:23:02 -04:00
Julian Krings d5b706764a isolate jigsaw component from objects 2025-08-27 16:12:12 +02:00
Julian Krings ca4c205a4a decrease wait times in mantle components 2025-08-27 16:08:49 +02:00
Julian Krings 7b9c2ae6ad minor optimizations 2025-08-27 12:34:52 +02:00
Julian Krings d0e9d44152 fix random null pointer in the resource loader 2025-08-27 00:33:59 +02:00
Julian Krings 2cdffaae33 include stronghold in mantle radius calc 2025-08-27 00:33:30 +02:00
Julian Krings d4a8beac95 fix more structures not being placed properly 2025-08-27 00:32:58 +02:00
Julian Krings 0e0e4075d8 fix more unsafe mantle operations 2025-08-27 00:32:34 +02:00
Julian Krings 9c492a2e66 save dev command for mantle panics 2025-08-26 17:46:27 +02:00
Julian Krings 96bf83684c add the shutdown hook on plugin enable instead of disable to prevent issues 2025-08-26 17:45:20 +02:00
Julian Krings 25ea9ae62d make regen respect multicore setting 2025-08-26 17:23:49 +02:00
Julian Krings 7938c150dd fix the DataContainer for the last time 2025-08-26 17:23:33 +02:00
Julian Krings a7b4bf3ff2 hopefully fix the DataContainer this time 2025-08-25 20:53:52 +02:00
Julian Krings 693a05f2cb move the updater command out of dev 2025-08-25 19:44:03 +02:00
Julian Krings e48cbe1f69 performance optimizations and add two experimental options for further optimizations 2025-08-24 22:33:35 +02:00
Julian Krings 558f6fa8dd Merge pull request #1218 from VolmitSoftware/fix/mantle
Mantle Fixes
2025-08-24 22:24:09 +02:00
Julian Krings 67b29cc363 cleanup palette and fix incorrect bit resizing 2025-08-24 18:54:44 +02:00
Julian Krings 4702534f9c add size check to prevent further corruption in case of read offset 2025-08-24 17:21:21 +02:00
Julian Krings 29390c5e0a fix potential issues in the CountingDataInputStream 2025-08-24 17:20:01 +02:00
Julian Krings e8bfce469d change the trimming cycle to make sure one step happens per second 2025-08-24 17:19:50 +02:00
Julian Krings 770e2f47a2 centralize file channels to prevent concurrent access 2025-08-24 17:19:39 +02:00
Julian Krings 747e7b3330 optimize the chunk updater 2025-08-21 17:27:07 +02:00
Julian Krings 8662f3b47a update ItemsAdder namespaces on load data 2025-08-21 13:50:21 +02:00
Aidan Aeternum 6e738e69e7 v+ 2025-08-21 02:40:05 -04:00
Aidan Aeternum 4bf14c83e1 Merge pull request #1215 from VolmitSoftware/dev
3.7.1
2025-08-21 02:39:19 -04:00
Julian Krings 837674c295 update sentry link 2025-08-19 18:00:22 +02:00
Julian Krings e5f3bbd69e make ItemsAdderDataProvider more reliable when adding items & blocks 2025-08-12 22:18:56 +02:00
Julian Krings 2885a39299 cleanup script engine 2025-08-09 00:00:31 +02:00
Julian Krings bd1b06c761 make studio map zoom in on the cursor target 2025-08-08 21:40:36 +02:00
Julian Krings 3e1143112a fix offset in studio map and some other issues 2025-08-08 21:38:26 +02:00
Julian Krings 834b214fbf add setting to control deposits replacing bedrock 2025-08-07 17:11:38 +02:00
Julian Krings 768e569400 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java
#	gradle/libs.versions.toml
2025-08-02 23:32:08 +02:00
Julian Krings a5d04333dd whoops messed up the structure slice 2025-08-02 23:19:11 +02:00
Julian Krings ebe8da0fd5 update slimjar for built-in spigot helper 2025-08-02 21:22:44 +02:00
Julian Krings 5eb25f3977 add property to use registry keys for where applicable
disabled by default for now
2025-08-02 00:01:33 +02:00
Julian Krings a5bca0a9bb fix not reading enum values correctly 2025-08-01 13:06:25 +02:00
Julian Krings a82882c1c1 move local maven repo into iris directory 2025-08-01 12:35:08 +02:00
Julian Krings 05193bd0d9 make depencency resolvers more predictable 2025-08-01 04:51:58 +02:00
Julian Krings 96efc15c36 fix local dependencies not resolving properly 2025-07-31 17:08:50 +02:00
Julian Krings 67f456cf53 cleanup script engine 2025-07-31 00:16:23 +02:00
Julian Krings 58b1bd115f update adventure api 2025-07-30 19:18:57 +02:00
Julian Krings 8ddc8abdb9 fix resolving dependencies in scripts 2025-07-30 16:54:19 +02:00
Julian Krings 12c2c71739 Merge branch 'refs/heads/dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/link/data/MythicMobsDataProvider.java
#	core/src/main/java/com/volmit/iris/util/misc/SlimJar.java
#	gradle/libs.versions.toml
2025-07-29 13:34:25 +02:00
Julian Krings 8705ca6e47 save structure placed at block 2025-07-28 21:44:27 +02:00
Julian Krings a610d0a7a9 remove unnecessary classpath injections 2025-07-28 20:53:01 +02:00
Julian Krings 0648cfd3fa add message for promotion of world to main world 2025-07-28 15:16:21 +02:00
Julian Krings fba9c17e3f fix not using relocated bukkit.yml correctly and remove duplicate code 2025-07-28 15:08:07 +02:00
Julian Krings 74128d58cf disable pack trimming for now 2025-07-28 15:06:34 +02:00
Julian Krings 70130e976d add getter method to IrisWorlds 2025-07-28 13:53:11 +02:00
Julian Krings 501c426302 fix slimjar error for new projects 2025-07-28 12:31:07 +02:00
Julian Krings 8471f15bc8 make world creation more failsafe 2025-07-27 23:23:04 +02:00
Julian Krings c4539441a0 fix datapack generation ignoring worlds when updating from <3.7.0 2025-07-27 23:22:40 +02:00
Julian Krings 472a98da16 update slimjar 2025-07-27 18:43:33 +02:00
Julian Krings 76a6f1465a update slimjar 2025-07-26 14:31:45 +02:00
Julian Krings b0ca0e3617 whoops messed up the extension retrieval 2025-07-25 13:21:16 +02:00
Julian Krings 08b9058c8f make git commit retrieval fail safe 2025-07-25 13:07:13 +02:00
Julian Krings 556bef6e43 fix more unsafe mantle chunk accesses 2025-07-25 12:44:01 +02:00
Julian Krings 16a4d20c90 use grgit to resolve current commit 2025-07-23 18:06:52 +02:00
Julian Krings e5f2fee926 add info about used commit to sentry reports 2025-07-23 15:07:06 +02:00
Julian Krings 3949468a60 inject into the library loader to make sure libraries can always be loaded 2025-07-23 14:45:54 +02:00
Julian Krings fc54fcb7eb make itemsadder data provider standalone 2025-07-22 12:12:13 +02:00
Julian Krings 9d6e4e87d8 fix custom blocks not placing 2025-07-22 12:11:41 +02:00
Julian Krings f50e964d4f hopefully fix unsafe mantle chunk operations 2025-07-21 17:14:45 +02:00
Julian Krings 8262e52893 cleanup regen 2025-07-19 18:10:57 +02:00
Julian Krings 1c41dc69ce Merge pull request #1213 from VolmitSoftware/feat/caves
Feat/caves
2025-07-19 17:59:32 +02:00
Julian Krings dee46a4284 cleanup object mask rotation 2025-07-19 17:48:58 +02:00
Julian Krings a8892b04ef bump nms for 1.21.8 support 2025-07-19 12:59:50 +02:00
Julian Krings d49f7d7821 add parameter to the create command to make it your main world 2025-07-13 17:43:11 +02:00
Julian Krings 106a3834ab fix iris create command failing 2025-07-13 17:42:10 +02:00
Julian Krings 387e8adfe2 ignore errors when scanning for external data providers 2025-07-12 18:31:39 +02:00
Julian Krings 9ade90d9ca make the mythic mobs link a data provider and make adding custom mob plugins easier 2025-07-12 15:51:49 +02:00
Julian Krings 3d2392843a update maven core to fix dependency resolution issues 2025-07-12 14:18:35 +02:00
Julian Krings a9891e819a v+ 2025-07-12 12:51:04 +02:00
Julian Krings 02c13a9391 Merge pull request #1211 from VolmitSoftware/dev
3.7.0
2025-07-12 12:42:38 +02:00
Julian Krings f6590c26e7 fix download failing for dimensions with snippets 2025-07-11 13:07:20 +02:00
Julian Krings 64bb81626c update overworld pack 2025-07-11 12:54:16 +02:00
Julian Krings 343dc429d5 fix null pointer when failing to load a dimension 2025-07-10 19:20:46 +02:00
Julian Krings 8f5f44bc96 open vscode after studio world was created 2025-07-10 18:24:50 +02:00
Julian Krings 6964b99744 fix reading custom mantle flags from json 2025-07-10 16:41:46 +02:00
Julian Krings 11cfd85f6a allow creation of custom mantle flags for custom mantle components 2025-07-10 16:11:02 +02:00
Julian Krings c5416f54fa add script hook for custom noise 2025-07-09 23:40:04 +02:00
Julian Krings 94c5782490 move loading message to debug 2025-07-09 18:49:55 +02:00
Julian Krings cc49b0f540 hopefully fix eta 2025-07-09 17:37:58 +02:00
Julian Krings 1bc6192c8b merge kts engine into this codebase 2025-07-09 15:16:11 +02:00
Julian Krings 4b0766c097 remove bfs 2025-07-09 14:24:24 +02:00
Julian Krings ec5cb2d646 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
2025-07-09 14:23:53 +02:00
Julian Krings 0edaeeec99 massive cleanup of maven repos and update dependencies 2025-07-08 23:40:10 +02:00
Julian Krings a0543bbbf2 relocate slimjar 2025-07-08 19:59:45 +02:00
Julian Krings 0b1af51227 add option to disable specific mantle components for testing 2025-07-08 19:52:09 +02:00
Julian Krings 03c5998c02 generate a dummy region for standalone focus biomes 2025-07-08 19:08:12 +02:00
Julian Krings 6b193f695a switch to slimjar 2025-07-08 18:46:59 +02:00
Julian Krings 0a30881f87 add jaxen to runtime dependencies 2025-07-08 16:08:30 +02:00
Julian Krings c01a7def5d add iris data setup hook 2025-07-08 15:37:14 +02:00
Julian Krings 50db1d11a7 move script engine libraries into the iris plugin folder 2025-07-08 12:30:21 +02:00
Julian Krings 2e2ea8f1e4 use paper plugin loader to avoid maven central warning 2025-07-08 09:19:57 +02:00
Julian Krings badf108d56 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java
#	core/src/main/java/com/volmit/iris/util/io/IO.java
#	core/src/main/resources/plugin.yml
2025-07-08 00:36:30 +02:00
Julian Krings f9888d19a5 whoops forgot to update publish task 2025-07-08 00:24:03 +02:00
Julian Krings fbc5cee300 if it still doesn't work, idk what will 2025-07-08 00:22:31 +02:00
Julian Krings 12777bc3f0 does this shit work now?? 2025-07-07 23:59:42 +02:00
Julian Krings 9324b1b5c0 fix api file generation 2025-07-07 23:42:30 +02:00
Julian Krings 2ecb555619 add publishing for the api artifact 2025-07-07 23:37:37 +02:00
Julian Krings 20c7891c2f add api generation task 2025-07-07 22:54:35 +02:00
Julian Krings c35c858eee add scripts for engine setup 2025-07-07 21:06:36 +02:00
Julian Krings cb93e78242 fix safeguard error 2025-07-07 13:18:31 +02:00
Julian Krings c9ed4519a8 remove multiverse core incompatibility warning (#1212) 2025-07-06 20:00:06 +02:00
Julian Krings 86d986dfbc fix deposits spawning in columns 2025-07-06 19:40:14 +02:00
Julian Krings 54402faea8 fix automatic deepslate variant replacement 2025-07-04 13:18:28 +02:00
Julian Krings 77b4253624 fix sentry safeguard info 2025-07-04 12:34:20 +02:00
Julian Krings 44af23ba2e add check for new dimension types 2025-07-04 00:06:55 +02:00
Julian Krings 4e8079e431 use ticket queue for pregen by default 2025-07-04 00:01:16 +02:00
Julian Krings 39f65d02bf 1.21.6/7 Support (#1210) 2025-07-03 23:58:31 +02:00
Julian Krings 8c025a9ba2 clear snippet lists on hotload 2025-07-03 23:45:27 +02:00
Julian Krings 7119fa8ef7 Merge pull request #1207 from VolmitSoftware/feat/byte_buddy
Feat/byte buddy
2025-07-03 23:24:30 +02:00
Julian Krings 6f0b2b6bba initialize generators for isolated focus biomes / regions 2025-06-27 12:21:59 +02:00
Julian Krings 0ae1334a57 fix snippet tab completion 2025-06-27 12:20:25 +02:00
Julian Krings 5d28563b7c implement generate surface for ores 2025-06-24 19:43:46 +02:00
Julian Krings cf8243a000 make caves more deterministic 2025-06-24 19:37:37 +02:00
Julian Krings cca0bed482 fix cave max size calculation 2025-06-24 16:53:26 +02:00
Julian Krings a802edc375 add some options for customizing caves 2025-06-23 17:16:45 +02:00
Julian Krings 3677931114 improve snippet finder 2025-06-23 15:51:53 +02:00
Julian Krings e38dae0a32 fix required properties for json schemas 2025-06-23 15:51:29 +02:00
Julian Krings d537459c5a Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle.kts
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-23 14:24:43 +02:00
Julian Krings 67398174bb cache iris worlds in a map for datapack generation 2025-06-23 12:25:39 +02:00
Julian Krings 0a0f77ff58 fix splash being shown twice 2025-06-23 12:25:38 +02:00
Julian Krings e5cb4d82a3 Add KGenerators support (#1209) 2025-06-21 12:05:56 +02:00
Julian Krings 9d44ac0b47 fix caves being non-deterministic 2025-06-21 12:04:39 +02:00
Julian Krings 7f6d65a13e fix worm ignoring the breakSurface option 2025-06-21 12:01:06 +02:00
Julian Krings ad720f4aa2 fix worm using xStyle for y and z 2025-06-21 11:51:49 +02:00
Julian Krings 80548f753c isolate java agent to prevent class loader issues 2025-06-21 11:48:58 +02:00
Julian Krings c9c8a9e412 fix null pointers in pack trim method 2025-06-16 22:53:56 +02:00
Julian Krings d048c073ac disable trim for fallback pack download 2025-06-16 22:53:11 +02:00
Julian Krings 2929a1f0a7 update Script Engine to e08b6f893e 2025-06-16 22:44:15 +02:00
Julian Krings b6f9f68b9f add global preprocessor setting 2025-06-16 22:43:26 +02:00
Julian Krings da2dd42e28 invert suppressed error reporting for dev enviroments 2025-06-16 16:59:24 +02:00
Julian Krings 88360ef772 Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-15 23:00:39 +02:00
Julian Krings 73787e21d2 Merge branch 'dev' into feat/kts
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/util/io/IO.java
2025-06-15 22:54:56 +02:00
Aidan Aeternum 840608a40f v+ 2025-06-14 16:49:22 -04:00
Aidan Aeternum fcedc35635 Merge pull request #1205 from VolmitSoftware/dev
3.6.11
2025-06-14 16:47:20 -04:00
Julian Krings 32d9a5e40a make sentry engine context hotload safe 2025-06-14 11:46:51 +02:00
Julian Krings 8df6253604 add safeguard info to sentry reports 2025-06-14 11:44:30 +02:00
Julian Krings 01b62c13b6 fix deleting mantle temp files before they are fully written 2025-06-14 11:18:14 +02:00
Julian Krings f32f73e65a disable error reporting in dev environment 2025-06-13 12:27:03 +02:00
Julian Krings 7b7118fe0d add id for servers hash of jvm name & version, processor info, memory size and plugins 2025-06-13 12:26:56 +02:00
Julian Krings 851ac18f0d implement custom conditions for mythic mobs (#1204) 2025-06-11 13:23:09 +02:00
Julian Krings 37be7ca847 don't send json and zip file closed exceptions to sentry 2025-06-11 13:14:28 +02:00
Julian Krings ed67b4d3c2 fix spawners not having entities due to using old format 2025-06-11 13:03:31 +02:00
Julian Krings b62ac875d5 cleanup gradle 2025-06-10 16:48:00 +02:00
Julian Krings 4c3f95b0e1 Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-10 13:33:56 +02:00
Julian Krings 8712c8874c disable global pregen cache by default for now 2025-06-09 23:27:12 +02:00
Julian Krings ccc3bab8e0 allow carving recursion 2025-06-09 23:25:33 +02:00
Julian Krings 113f25dab8 more sentry context 2025-06-09 23:17:47 +02:00
Julian Krings cd80acdc7d fix IndexOutOfBoundsException when getting the selection from the wand 2025-06-09 19:50:50 +02:00
Julian Krings 9316ea9e5b fix npe when creating cuboids 2025-06-09 19:49:06 +02:00
Julian Krings e2a3f25dcb fix load in parallel method 2025-06-09 19:42:03 +02:00
Julian Krings 944cc19ebc use proper shuffling algorithm for the loot 2025-06-09 19:35:43 +02:00
Julian Krings 172e234514 fix divide by zero in the engine svc 2025-06-09 19:19:00 +02:00
Julian Krings f9dac8a3a1 fix compile while missing the sentry auth token 2025-06-09 18:40:43 +02:00
Julian Krings eefbbab7ee remove legacy chars before minimessage deserialization 2025-06-09 18:39:11 +02:00
Julian Krings 9cf567e1ff remove unused util to fix class not found exception 2025-06-09 13:47:24 +02:00
Julian Krings b8ee7561dd fix failing to create temp file when user deleted the temp directory 2025-06-09 13:44:57 +02:00
Julian Krings c0d17742e8 fix release task for sentry 2025-06-09 13:43:00 +02:00
Julian Krings a88d389e0f bump gradle wrapper to 8.14.2 and switch to kotlin dsl (#1203) 2025-06-09 12:45:27 +02:00
Julian Krings b341089996 v+ 2025-06-09 02:12:14 +02:00
Julian Krings 9cbfd5a10b Merge pull request #1202 from VolmitSoftware/dev
3.6.10
2025-06-09 02:11:22 +02:00
Julian Krings fdaf8ff9d3 relocate sentry to fix incompatibility with some plugins using a very old sentry version 2025-06-09 00:17:38 +02:00
Julian Krings e63d84c052 remove server name from reports 2025-06-09 00:16:58 +02:00
Julian Krings 0d103a934a fix ticking engine players in the wrong dimension 2025-06-08 12:43:56 +02:00
Julian Krings 8119207254 sentry changes
- info message on enabling
- add release tag
- disable uncaught exception handling to prevent reporting other plugins issues
2025-06-08 12:29:24 +02:00
Julian Krings b66e6d8335 correct sentry dsn 2025-06-07 14:00:22 +02:00
Julian Krings ce2b62f5ae add sentry release task 2025-06-06 18:37:11 +02:00
Julian Krings c767b6c8e8 check sentry env token as fallback 2025-06-06 18:36:41 +02:00
Julian Krings 67328d7d10 remove test exception for sentry 2025-06-06 17:35:13 +02:00
Julian Krings 329e136a66 implement opt-out auto reporting with sentry (#1201) 2025-06-06 17:33:16 +02:00
Julian Krings 52f87befa2 return noop pregen cache when the service is disabled and write pregen cache to temp file first then replace real one 2025-06-06 17:24:23 +02:00
Julian Krings 2ee22db072 fix mantle write failing on windows 2025-06-06 16:49:42 +02:00
Julian Krings cc27e87376 fix world height decreases deleting the whole chunk 2025-06-05 20:34:25 +02:00
Julian Krings abb1d9cd62 add bytebuddy binding for 1.21.5 2025-06-05 20:26:16 +02:00
Julian Krings e0ad029c3d Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java
2025-06-05 19:27:01 +02:00
Julian Krings 5705caa1ba fix benchmarking not disabling properly 2025-06-04 20:20:48 +02:00
Julian Krings a56cd4c268 use block data use for slice 2025-06-04 16:32:29 +02:00
Julian Krings 48cc6bb49c improve mantle writer speeds 2025-06-04 15:03:00 +02:00
Aidan Aeternum 635ee02459 v+ 2025-06-02 18:48:01 -04:00
Aidan Aeternum 2c7b7c8c91 Merge pull request #1199 from VolmitSoftware/dev
3.8.9
2025-06-02 18:47:38 -04:00
Julian Krings ad0662be54 initial 1.21.5 bindings 2025-06-02 18:18:17 +02:00
Julian Krings 835c8422f1 calculate spawn location async 2025-06-02 17:42:36 +02:00
Julian Krings 2c60192de3 use scheduled thread pool instead of loopers for the EngineSVC 2025-06-02 17:29:38 +02:00
Julian Krings adb7188eb9 refactor mantle cleanup 2025-06-02 17:29:30 +02:00
Julian Krings 2436ebb857 Merge pull request #1198 from VolmitSoftware/feat/versioned_mantle 2025-06-02 17:28:58 +02:00
Julian Krings ad85a0bbd1 implement marker exhaustionChance 2025-05-29 23:17:23 +02:00
Julian Krings 22ac9ebf47 update nexo api to release 1.6.0 2025-05-29 14:18:30 +02:00
Julian Krings 3cb9585dd8 fix pregenerator not closing when a new one is started 2025-05-28 21:48:41 +02:00
Julian Krings f4d1177c51 fix cave fluid ignoring fluid palette 2025-05-28 12:57:11 +02:00
Julian Krings b132862a60 update last use for in use tectonic plates 2025-05-27 22:50:30 +02:00
Julian Krings 2452a2f633 cleanup of the mantle trimmer / EngineSVC 2025-05-27 16:23:02 +02:00
Julian Krings f0476fea9b implement version header for tectonic plates 2025-05-26 20:54:12 +02:00
Julian Krings 90c6457d37 write to plates to a temp file first and then move it into the mantle dir 2025-05-26 17:29:15 +02:00
Julian Krings 61301ffd4d close engines using a shutdown hook 2025-05-26 17:29:15 +02:00
Julian Krings e42317139d fix engines not closing on server stop 2025-05-26 17:29:15 +02:00
Julian Krings f68600464b remove redundant method from NMSBinding1X 2025-05-25 18:12:48 +02:00
Julian Krings 97ddfd309b fix updater not working 2025-05-21 13:05:07 +02:00
Aidan Aeternum 5d42c5cae0 v+ 2025-05-20 12:32:04 -04:00
Aidan Aeternum 52fecb48d8 Merge pull request #1196 from VolmitSoftware/dev
3.6.8
2025-05-20 12:31:35 -04:00
Julian Krings ce29dc98c3 fix spawn markers not being removed 2025-05-16 12:41:31 +02:00
Julian Krings 463e3d9658 Merge pull request #1193 from VolmitSoftware/feat/faster_pregen
Feat/faster pregen
2025-05-16 12:29:41 +02:00
Julian Krings 9152b25d51 fix pack hash calculation 2025-05-16 12:22:35 +02:00
Julian Krings bb9c72e414 Partially Merge pull request #1192 from dan28000/Iris 2025-05-16 12:11:46 +02:00
Julian Krings 976648340e add pregen method that doesn't use waiting threads 2025-05-16 12:02:38 +02:00
Julian Krings 5958bcb22e update Script Engine to 41396a77d5 2025-04-28 21:40:44 +02:00
Julian Krings 8eb35aa8be update Script Engine to b5e2cc6e 2025-04-28 21:24:54 +02:00
Julian Krings 49ce84a9e9 Merge branch 'dev' into feat/faster_pregen
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java
2025-04-27 13:56:28 +02:00
Julian Krings 6724b0f4c5 minor cleanup and improved safeguard for missing dimension types 2025-04-25 21:18:33 +02:00
Julian Krings e72abc8c39 generate json schemas for intelij 2025-04-24 16:00:23 +02:00
Julian Krings 0a2f35dd8d update the scripting engine and fix cached file extension 2025-04-23 21:24:14 +02:00
Julian Krings c597c55c2c replace rhino scripting engine with kotlin scripting 2025-04-23 14:21:09 +02:00
Aidan Aeternum f93995e152 v+ 2025-04-21 20:27:43 -04:00
Aidan Aeternum 7a5a2e5909 Merge pull request #1191 from VolmitSoftware/dev
3.6.7
2025-04-21 20:27:18 -04:00
dan28000 e15d31a0ed would be nice to display the right number 2025-04-21 22:59:29 +02:00
dan28000 09cdd61a68 fix method
when having relocated worlds folder this method would fail making warning appear
2025-04-21 20:45:20 +02:00
dan28000 0575cd85c8 delete old stuff
delete legacy command
2025-04-21 20:44:38 +02:00
Julian Krings 2008975a8a expose most of the dimension type options 2025-04-17 17:30:29 +02:00
Julian Krings aad1d3815a Merge branch 'dev' into feat/byte_buddy 2025-04-16 23:18:49 +02:00
Julian Krings 86c64f99e9 filter out non iris pack directories in allPacks stream 2025-04-16 23:01:57 +02:00
Julian Krings 6577f4a5de rough safeguard for missing dimension type 2025-04-16 22:02:42 +02:00
Julian Krings 3415e7c7af implement complete world height isolation 2025-04-16 21:52:30 +02:00
Pixel f1c72974fd Cleanup (#1186)
* Remove server benchmark command

* Moved lazy pregen to dev
2025-04-15 15:53:50 +02:00
Julian Krings 26e2e20840 Feat/pregen cache (#1190)
Add cache to pregen to skip already generated chunks faster
2025-04-14 21:26:39 +02:00
Julian Krings c00dcf205b fix deadlock when closing pregen method while using modified concurrency 2025-04-14 21:25:31 +02:00
Julian Krings a10a784c3b add max concurrency setting for pregen 2025-04-14 21:25:31 +02:00
Julian Krings f99cc61042 add setting to use a virtual thread executor instead of MultiBurst for async pregen 2025-04-14 21:25:31 +02:00
Julian Krings d2a1e5cc1e fix duplicate & redundant purpur recommendations 2025-04-14 21:23:52 +02:00
Julian Krings 3e2c0fa025 Decrease MSPT impact of throwing ender eyes in Iris worlds 2025-04-14 21:23:37 +02:00
Julian Krings 9c151abac7 replace world load context injection with bytecode injections 2025-04-10 16:26:58 +02:00
Julian Krings 566fca2b52 Add info message for failed increase of worker threads 2025-04-07 19:00:06 +02:00
Julian Krings fb0bc112e3 increase worker threads on paper servers during pregen 2025-03-30 14:31:11 +02:00
2796 changed files with 393113 additions and 124334 deletions
+29 -11
View File
@@ -27,22 +27,40 @@ body:
placeholder: The code to place a is missing b and c...
validations:
required: false
- type: dropdown
id: platform
attributes:
label: Platform
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
options:
- Paper
- Purpur
- Leaf
- Canvas
- Folia
- Spigot / CraftBukkit
- Fabric
- Forge
- NeoForge
- Other (describe under Problem)
validations:
required: true
- type: dropdown
id: mcversion
attributes:
label: Minecraft Version
description: What version of Minecraft is the server on?
description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported.
options:
- 1.14.X
- 1.15.X
- 1.16.X
- 1.17
- 1.17.1
- 1.18
- 1.19
- 1.20
- 1.21
- 1.22
- '26.2'
- Other (unsupported)
validations:
required: true
- type: input
id: loaderversion
attributes:
label: Platform / Loader Version
description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta".
placeholder: DO NOT SAY "LATEST"
validations:
required: true
- type: input
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (core, plugin, spi, probes)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run verification gates
run: ./gradlew :core:check :adapters:bukkit:plugin:test :adapters:bukkit:nms:v26_2_R1:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Fabric)
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Forge)
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (NeoForge)
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Test artifact verifiers
run: ./gradlew -p buildSrc test --console=plain --stacktrace
- name: Verify Bukkit artifact
run: ./gradlew verifyBukkitArtifact --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Verify modded artifacts
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
+61 -1
View File
@@ -10,4 +10,64 @@ libs/
collection/
/core/src/main/java/com/volmit/iris/util/uniques/
out/
tools/simd-bench/out/
tools/simd-bench/simd-bench.jar
DataPackExamples/
TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is
# server output; neither is ever source. No source directory in this repo is named run or logs.
run/
logs/
*.log
*.log.gz
crash-reports/
hs_err_pid*.log
replay_pid*.log
.codegraph/
plans/
.env
.env.*
!.env.example
local.properties
*.jks
*.p12
*.pfx
*.pem
*.key
*.hprof
credentials.json
service-account*.json
# docs/ is hand-written and tracked (flat numbered product docs). Nothing under it is
# generated, so there is nothing to ignore here.
.qa/
.repro/
.perf/
.release-gate/
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
.apiwt/
.papiwt/
__pycache__/
# Doc authoring scratch
.docs-wip/
AGENTS.md
# Local build tooling, not part of the project
build-lt.sh
BUILDS/
.run/
+280 -54
View File
@@ -1,75 +1,301 @@
# Iris
The master branch is for the latest version of minecraft.
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
or NeoForge server mod. Cross-platform generation targets deterministic parity for identical
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2.
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [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)
# Building
Authoritative docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository, which is the source for the hosted wiki.
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.
Consider supporting development by buying Iris on Spigot.
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
## Language and localization
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/core/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
### Command Line Builds
## Platforms
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 21
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
3. Once the project has setup, run `gradlew iris`
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set |
| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 |
### IDE Builds (for development)
Java 25 is required on every platform.
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
prefer.
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
workspaces with schema autocomplete over the server's live registries, entity spawning parity
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
the goldenhash determinism gate, which is interchangeable across all four platforms.
# Iris Toolbelt
### Native worldgen over Iris terrain
Everyone needs a tool-belt.
Iris replaces the chunk generator, so vanilla and mod worldgen only runs where Iris runs it. This
is identical on every platform.
```java
package com.volmit.iris.core.tools;
| Vanilla / mod worldgen | Over Iris terrain | Control |
|---|---|---|
| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies individual keys |
| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters |
| Carvers (caves, canyons, mod carvers) | Never - architectural | Iris has no `NoiseGeneratorSettings` for a carver to sample; use pack `caves`/`carvings` |
| Surface builders and surface rules | Never | Iris builds its surface from pack palettes |
| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter` or `biomeSkyScatter` target | Iris chooses biomes from the pack, not from a biome source |
| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's |
// Get IrisDataManager from a world
IrisToolbelt.access(anyWorld).getCompound().getData();
With `importedFeatures` off - the default - chunk output is byte-for-byte what Iris has always
produced. See [94 - API - Modded](https://github.com/VolmitSoftware/docs/blob/master/iris/94-api-modded.md) and
[01 - Installation & Platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) for the full control reference, including
which `pointed_dripstone` keys the 26.2 `speleothem` rename does and does not affect.
// Get Default Engine from world
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
Independently of that flag, Iris custom biomes now inherit the biome tags of their vanilla
derivative on every platform, so the emitted datapack tag files change. Anything driven by biome
tags therefore applies to Iris custom biomes: mob variants, spawn rules, and any vanilla or mod
content selecting on `#minecraft:is_overworld` and friends.
// Get the engine at the given height
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
## Install
// IS THIS THING ON?
boolean yes=IrisToolbelt.isIrisWorld(world);
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
the server. First boot performs no pack download. Run `/iris download pack=overworld`,
`/iris download pack=underworld`, or `/iris download link=https://host/path/pack.zip`, waiting for
each download to finish before starting another. The current built-in packs declare no external
datapack imports. Restart after download so Minecraft loads their dimension types and biomes.
Custom packs that declare `datapackImports` must complete their external-datapack installation and
registry restart first. Plain Spigot supports ordinary managed `/iris create`, but not
the early-bootstrap `/iris replace` path for canonical Overworld, Nether, or End slots.
// GTFO for worlds (moves players to any other world, just not this one)
IrisToolbelt.evacuate(world);
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only
packs already on disk and never accesses the network. `/iris download` installs a pack atomically
without stopping the server. The current built-in packs declare no external datapack imports.
Custom packs that declare them require compatible archives in that save's `datapacks/` directory;
modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart only after
the Iris pack and all of its inputs are present. Packs register their custom dimension types,
height ranges, biomes, and external structure keys during that boot; worlds created
before it run with fallback registry data.
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
Create New World screen; the integrated server runs the same engine.
## The client mod
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
mod is inert.
## Quickstart
Complete the platform's pack, external-datapack, and registry-restart workflow above first. Then
create and enter an Iris world.
Plugin (optional arguments are keyed):
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
.name("myWorld") // The world name
.dimension("terrifyinghands")
.seed(69133742) // The world seed
.pregen(PregenTask // Define a pregen job to run
.builder()
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
.build())
.create();
```
/iris create myworld type=overworld seed=1337
/iris tp myworld
```
Mod (positional arguments):
```
/iris create myworld overworld 1337
/iris tp irisworldgen:myworld
```
On Paper-family servers with early bootstrap (not plain Spigot), the shipping pair can instead
replace the canonical portal-linked slots after their registry workflow is complete:
```
/iris replace minecraft:overworld type=overworld seed=123456789
/iris replace minecraft:the_nether type=underworld seed=-987654321
```
Restart once after both commands report staged. The independent seeds apply to their respective
slots, and vanilla portals keep routing between the canonical Overworld and Nether identities.
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
servers they are positional and composable:
```
/iris pregen start 352 world=myworld center=0,0 gui=false
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
```
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
console/status output. `/iris pregen status` reports progress on the plugin.
## Studio and VSCode workspace
The studio is the pack authoring environment, available on all platforms. Studio worlds are
transient - they are deleted on close and purged at startup.
Plugin:
```
/iris studio create name=<name> [template=<pack>]
/iris studio open <pack> [seed=1337]
/iris studio vscode [dimension=<pack>]
/iris studio update [dimension=<pack>]
/iris studio close
```
Mod:
```
/iris studio create [name] [template]
/iris studio open <pack> [seed]
/iris studio vscode [pack]
/iris studio update [pack]
/iris studio close
```
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
files hotloads the changes and regenerates the schemas.
## PlaceholderAPI
Iris registers the `iris` expansion when PlaceholderAPI is enabled. Paths are dot-separated,
lowercase, and never contain an underscore. Every value is plain text: no colour codes, no unit
suffixes, no `%` character, `.` as the decimal separator, and no thousands grouping.
Three answers are possible. A path that is not in the list below returns nothing, so PlaceholderAPI
re-emits the literal `%iris_...%` and a typo stays visible. A known path with no value right now
returns `---`. A real zero returns `0`.
| Placeholder | Value |
|---|---|
| `%iris_available%` | `true` when the Iris terrain service is live |
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
| `%iris_world.biome%` | Surface biome display name at the player, e.g. `Hot Desert Dunes` |
| `%iris_world.biome-key%` | Surface biome load key, e.g. `desert/hot-dunes` |
| `%iris_world.region%` | Region display name at the player |
| `%iris_world.region-key%` | Region load key |
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
| `%iris_pregen.available%` | `true` while a pregeneration job is running |
| `%iris_pregen.world%` | World name the running job is pregenerating |
| `%iris_pregen.percent%` | Completion, `0.00` to `100.00`, no `%` character |
| `%iris_pregen.eta%` | Estimated seconds remaining, whole number |
| `%iris_pregen.eta-text%` | Same estimate as `2m 5s` or `1h 30m` |
| `%iris_pregen.chunks%` | Chunks generated so far |
| `%iris_pregen.total%` | Chunks in the job |
| `%iris_pregen.chunks-per-second%` | Current rate |
| `%iris_pregen.paused%` | `true` while the job is paused |
The world values are the surface reading at the player's block column. Walking refreshes them at most
once per second per player, so a whole board of `world.*` keys costs one refresh per player per
second no matter how many of them are on it, and a value may lag a sprinting player by up to a
second. A jump that is not walking - joining, respawning, changing worlds, stepping through a portal,
or any teleport including `/iris goto`, `/tp`, an ender pearl and a random teleport - is published
immediately, so a player who arrives somewhere and then stands still never keeps reading the biome,
region or dimension of where they came from. `pregen.*` is global: there is one pregeneration job per
server, and `%iris_pregen.world%` says which world it is.
### Migration from the pre-2.0 keys
The old underscore keys are gone. There is no alias and no dual-accept window; an old key now
renders literally so it is visible rather than silently wrong.
| Old key | New key | Why |
|---|---|---|
| `%iris_biome_name%` | `%iris_world.biome%` | Renamed onto the dot grammar |
| `%iris_biome_id%` | `%iris_world.biome-key%` | Renamed; `id` was always the load key |
| `%iris_region_name%` | `%iris_world.region%` | Renamed onto the dot grammar |
| `%iris_region_id%` | `%iris_world.region-key%` | Renamed; `id` was always the load key |
| `%iris_biome_file%` | removed | Rendered an absolute server path into player-visible text, and threw on packs with no backing file |
| `%iris_region_file%` | removed | Same as `biome_file` |
| `%iris_world_seed%` | removed | Handed the world seed to anyone who could read a scoreboard, and a placeholder has no permission context to gate on |
| `%iris_terrain_height%` | removed | Reported the *generated* height, before objects and player edits, so it disagreed with the block under the player's feet |
| `%iris_terrain_slope%` | removed | Three extra noise samples per read for an unformatted pack-authoring diagnostic |
| `%iris_world_mode%` | removed | Studio or Production; a studio world exists for seconds during authoring and is never on a live board |
| `%iris_world_speed%` | removed | Mutated engine rate-window state every time it was read. `%iris_pregen.chunks-per-second%` answers the same question from a snapshot |
The old keys also read the *cave* biome for a player standing under an overhang, because they
sampled two blocks above the player's feet. The new `world.biome` is always the surface biome,
which is what a board reader means.
## Building from source
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a
consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo;
override with `-Plocation=/path/to/consumers`.
If you need help compiling as a developer or contributor, ask in the Discord.
## Adapters / modded development
`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three
modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds
with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one
plugin classpath. Drive them with `-p`:
```
./gradlew -p adapters/fabric runServer # or runClient
./gradlew -p adapters/forge runServer
./gradlew -p adapters/neoforge runServer
./gradlew -p adapters/fabric test # shared adapters/modded-common test suite
```
Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system
properties:
| Flag | System property | Purpose |
|---|---|---|
| `-PirisParity=<pack>` | `iris.parity` | Run the cross-platform parity harness for a pack |
| `-PirisParityGolden=<file>` | `iris.parity.golden` | Compare against a captured golden-hash file |
| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only |
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
screens); every adapter adds those source directories, so one edit reaches all three loaders.
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
substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a
composite cycle. The build and release paths do not need it.
## Documentation
Full product docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository. Start with:
- [Overview and index](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md)
- [Installation and platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md)
- [Getting started](https://github.com/VolmitSoftware/docs/blob/master/iris/02-getting-started.md)
- [Structures overview](https://github.com/VolmitSoftware/docs/blob/master/iris/18-structures-overview.md)
- [API — Getting Started](https://github.com/VolmitSoftware/docs/blob/master/iris/90-api-getting-started.md)
- [Maintainer — MC version bump](https://github.com/VolmitSoftware/docs/blob/master/iris/85-maintainer-mc-version-bump.md)
- [Maintainer — release checklist](https://github.com/VolmitSoftware/docs/blob/master/iris/86-maintainer-release-checklist.md)
@@ -0,0 +1,743 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftWorld;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
private static final int NOISE_BIOME_CACHE_MAX = 262144;
private static final int STRONGHOLD_RING_SEARCH_Y = 0;
private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112;
private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4;
private final long seed;
private final Engine engine;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
private volatile int cacheRuntimeId;
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
this.seed = seed;
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
this.cacheDimension = engine.getDimension();
this.cacheRuntimeId = engine.getCacheID();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
}
biomes.add(vanillaHolder);
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder == null) {
throw new IllegalStateException("Iris custom structure biome '"
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
}
biomes.add(customHolder);
}
}
}
if (biomes.isEmpty()) {
throw new IllegalStateException("Iris pack '" + engine.getName()
+ "' has no registered structure biomes");
}
return new ArrayList<>(biomes);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
if (o != null) {
return o;
}
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object fieldFor(Class<?> returns, Object in) {
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
private static Object invokeFor(Class<?> returns, Object in) {
for (Method i : in.getClass().getMethods()) {
if (i.getReturnType().equals(returns)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
for (Field i : sourceType.getDeclaredFields()) {
if (i.getType().equals(returnType)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
return null;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleStructureBiomes().stream();
}
Set<Holder<Biome>> possibleStructureBiomes() {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
if (lease == null) {
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
KMap<String, Holder<Biome>> m = new KMap<>();
if (customRegistry == null) {
return m;
}
for (IrisBiome i : engine.getAllBiomes()) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (holder == null) {
if (fallback != null) {
m.put(j.getId(), fallback);
}
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
continue;
}
m.put(j.getId(), holder);
}
}
}
return m;
}
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
if (customRegistry == null || registry == null) {
return Collections.unmodifiableMap(spawnBiomes);
}
for (IrisBiome irisBiome : engine.getAllBiomes()) {
if (!irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
return Collections.unmodifiableMap(spawnBiomes);
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
if (biome == null) {
return null;
}
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
return null;
}
ensureCachesCurrent();
return vanillaSpawnBiomes.get(biome.value());
}
}
private RegistryAccess registry() {
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
}
@Override
public Pair<BlockPos, Holder<Biome>> findBiomeHorizontal(
int x,
int y,
int z,
int searchRadius,
Predicate<Holder<Biome>> allowed,
RandomSource random,
Climate.Sampler sampler
) {
int quartStep = horizontalBiomeSearchQuartStep(y, searchRadius);
if (quartStep == 1) {
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
}
return super.findBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
}
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
? STRONGHOLD_RING_SEARCH_QUART_STEP
: 1;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within");
if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
}
}
return biomes;
}
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
return internalY > caveSwitchInternalY;
}
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
prepareVisibleBiomeBatch();
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
}
}
void prepareVisibleBiomeBatch() {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler
) {
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState
) {
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) {
return null;
}
try {
return engine.acquireGenerationLease(operation);
} catch (GenerationSessionException e) {
if (engine.isClosing() || e.isExpectedTeardown()) {
return null;
}
throw new IllegalStateException("Iris biome source could not acquire generation session for "
+ operation + ".", e);
}
}
private boolean isRuntimeAvailable() {
return !engine.isClosed() && engine.getComplex() != null;
}
private void ensureCachesCurrent() {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
}
synchronized (this) {
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
}
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
cacheRuntimeId = runtimeId;
}
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z);
}
Holder<Biome> holder = resolveBiomeHolder(
biomeRegistry, resolution.irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
}
return holder;
}
private Holder<Biome> resolveVisibleBiomeHolder(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
if (resolution == null) {
return getFallbackBiome();
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
org.bukkit.block.Biome vanillaBiome = resolution.underground
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
if (holder != null) {
return holder;
}
return getFallbackBiome();
}
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
if (holder != null) {
return holder;
}
}
return getFallbackBiome();
}
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null);
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
if (engine == null || engine.isClosed()) {
return null;
}
if (engine.getComplex() == null) {
return null;
}
int blockX = x << 2;
int blockZ = z << 2;
int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean underground = false;
IrisBiome irisBiome;
if (deepUnderground) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null) {
return null;
}
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
}
private BiomeResolution createBiomeResolution(
IrisBiome irisBiome,
boolean underground,
int blockX,
int blockY,
int blockZ
) {
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
}
private Holder<Biome> getFallbackBiome() {
if (fallbackBiome != null) {
return fallbackBiome;
}
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
if (holder != null) {
return holder;
}
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static long packColumnKey(int x, int z) {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
return null;
}
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
customBiomeId.toLowerCase(java.util.Locale.ROOT)
);
Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) {
return null;
}
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
return null;
}
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
if (optionalReferenceHolder.isEmpty()) {
return null;
}
return optionalReferenceHolder.get();
}
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(biomeKey);
if (identifier == null) {
return null;
}
return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null);
}
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
if (plains != null) {
return plains;
}
Holder<Biome> vanilla = firstHolder(registry);
if (vanilla != null) {
return vanilla;
}
return firstHolder(customRegistry);
}
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
for (Biome biome : registry) {
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
continue;
}
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
if (optionalHolder.isPresent()) {
return optionalHolder.get();
}
}
return null;
}
private static final class BiomeResolution {
private final IrisBiome irisBiome;
private final boolean underground;
private final int blockX;
private final int blockY;
private final int blockZ;
private final RNG rng;
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
this.irisBiome = irisBiome;
this.underground = underground;
this.blockX = blockX;
this.blockY = blockY;
this.blockZ = blockZ;
this.rng = rng;
}
}
}
@@ -0,0 +1,509 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.framework.NativeStructureFrequencyScale;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class DatapackStructureStateFilter {
private DatapackStructureStateFilter() {
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl());
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures
) {
return filter(
structureSets,
scopeIndex,
declaredSources,
importedStructures,
keyIndex(structureSets));
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (scopeIndex.isEmpty()) {
return new Selection(
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
0,
0);
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity = new IdentityHashMap<>();
Set<Holder<StructureSet>> visiting = Collections.newSetFromMap(new IdentityHashMap<>());
List<Holder<StructureSet>> filteredSets = new ArrayList<>(structureSets.size());
int retainedManagedSets = 0;
int excludedManagedSets = 0;
for (Holder<StructureSet> holder : structureSets) {
String setKey = structureSetKey(holder, keyIndex);
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
Holder<StructureSet> scopedHolder = scopeHolder(
holder,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (scopedHolder == null) {
if (managedSet) {
excludedManagedSets++;
}
continue;
}
if (managedSet) {
retainedManagedSets++;
}
filteredSets.add(scopedHolder);
}
return new Selection(
scaleFrequencyOverrides(
List.copyOf(filteredSets), importedStructures, keyIndex),
retainedManagedSets,
excludedManagedSets);
}
static String structureSetKey(Holder<StructureSet> holder) {
return structureSetKey(holder, keyIndex(List.of(holder)));
}
static StructureSetKeyIndex keyIndex(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
return StructureSetKeyIndex.create(registeredStructureSets);
}
private static String structureSetKey(
Holder<StructureSet> holder,
StructureSetKeyIndex keyIndex
) {
return keyIndex.resolve(holder)
.map(key -> key.identifier().toString())
.orElse(null);
}
private static List<StructureSet.StructureSelectionEntry> filterEntries(
List<StructureSet.StructureSelectionEntry> entries,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
List<StructureSet.StructureSelectionEntry> filtered = new ArrayList<>(entries.size());
for (StructureSet.StructureSelectionEntry entry : entries) {
String structureKey = entry.structure().unwrapKey()
.map(key -> key.identifier().toString())
.orElse(null);
if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) {
filtered.add(entry);
}
}
return filtered;
}
private static Holder<StructureSet> scopeHolder(
Holder<StructureSet> holder,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
if (scopedByIdentity.containsKey(holder)) {
return scopedByIdentity.get(holder);
}
if (!visiting.add(holder)) {
return null;
}
try {
String setKey = structureSetKey(holder, keyIndex);
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
scopedByIdentity.put(holder, null);
return null;
}
StructureSet originalSet = holder.value();
List<StructureSet.StructureSelectionEntry> entries = filterEntries(
originalSet.structures(), scopeIndex, declaredSources);
if (entries.isEmpty()) {
scopedByIdentity.put(holder, null);
return null;
}
StructurePlacement placement = scopePlacement(
originalSet.placement(),
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (entries.size() == originalSet.structures().size()
&& placement == originalSet.placement()) {
scopedByIdentity.put(holder, holder);
return holder;
}
Holder<StructureSet> scoped = replacementHolder(
holder, new StructureSet(entries, placement), keyIndex);
scopedByIdentity.put(holder, scoped);
return scoped;
} finally {
visiting.remove(holder);
}
}
static Optional<StructurePlacement.ExclusionZone> exclusionZone(StructurePlacement placement) {
Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class);
if (value instanceof Optional<?> optional && (optional.isEmpty()
|| optional.get() instanceof StructurePlacement.ExclusionZone)) {
@SuppressWarnings("unchecked")
Optional<StructurePlacement.ExclusionZone> resolved =
(Optional<StructurePlacement.ExclusionZone>) optional;
return resolved;
}
throw new IllegalStateException("Could not read structure placement exclusion zone from "
+ placement.getClass().getName());
}
private static StructurePlacement scopePlacement(
StructurePlacement placement,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
Optional<StructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
if (currentZone.isEmpty()) {
return placement;
}
Holder<StructureSet> target = currentZone.get().otherSet();
String targetKey = structureSetKey(target, keyIndex);
if (targetKey != null) {
target = holdersByKey.getOrDefault(targetKey, target);
}
Holder<StructureSet> scopedTarget = scopeHolder(
target,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
Optional<StructurePlacement.ExclusionZone> scopedZone = Optional.empty();
if (scopedTarget != null) {
scopedZone = Optional.of(new StructurePlacement.ExclusionZone(
scopedTarget,
currentZone.get().chunkCount()));
}
if (scopedTarget == currentZone.get().otherSet()) {
return placement;
}
return copyPlacement(placement, scopedZone, 1D);
}
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
List<Holder<StructureSet>> structureSets,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (!importedStructures.hasFrequencyOverrides()) {
return structureSets;
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
boolean changed = false;
for (Holder<StructureSet> holder : structureSets) {
Holder<StructureSet> scaled = scaleFrequencyHolder(
holder,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
scaledSets.add(scaled);
changed |= scaled != holder;
}
return changed ? List.copyOf(scaledSets) : structureSets;
}
private static Holder<StructureSet> scaleFrequencyHolder(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
StructureSetKeyIndex keyIndex
) {
if (scaledByIdentity.containsKey(holder)) {
return scaledByIdentity.get(holder);
}
if (!dependsOnFrequencyOverride(
holder, importedStructures, holdersByKey, keyIndex)) {
scaledByIdentity.put(holder, holder);
return holder;
}
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
new IllegalStateException(
"An affected native structure-set exclusion graph has an unkeyed holder"));
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
scaledByIdentity.put(holder, scaledHolder);
StructureSet originalSet = holder.value();
StructurePlacement originalPlacement = originalSet.placement();
Optional<StructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
Optional<StructurePlacement.ExclusionZone> scaledZone = originalZone;
if (originalZone.isPresent()) {
Holder<StructureSet> target = canonicalHolder(
originalZone.get().otherSet(), holdersByKey, keyIndex);
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
target,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
if (scaledTarget != originalZone.get().otherSet()) {
scaledZone = Optional.of(new StructurePlacement.ExclusionZone(
scaledTarget, originalZone.get().chunkCount()));
}
}
double multiplier = importedStructures.frequencyMultiplier(
structureSetKey(holder, keyIndex));
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
? originalPlacement
: copyPlacement(originalPlacement, scaledZone, multiplier);
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
return scaledHolder;
}
private static boolean dependsOnFrequencyOverride(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Holder<StructureSet> current = holder;
while (visited.add(current)) {
if (importedStructures.frequencyMultiplier(
structureSetKey(current, keyIndex)) != 1D) {
return true;
}
Optional<StructurePlacement.ExclusionZone> zone =
exclusionZone(current.value().placement());
if (zone.isEmpty()) {
return false;
}
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
}
return false;
}
private static Holder<StructureSet> canonicalHolder(
Holder<StructureSet> holder,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
String key = structureSetKey(holder, keyIndex);
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
}
private static Holder<StructureSet> replacementHolder(
Holder<StructureSet> original,
StructureSet replacement,
StructureSetKeyIndex keyIndex
) {
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
if (key.isEmpty()) {
return Holder.direct(replacement);
}
ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get());
holder.bind(replacement);
return holder;
}
private static StructurePlacement copyPlacement(
StructurePlacement placement,
Optional<StructurePlacement.ExclusionZone> exclusionZone,
double frequencyMultiplier
) {
Vec3i locateOffset = (Vec3i) declaredFieldValue(
StructurePlacement.class, placement, Vec3i.class);
StructurePlacement.FrequencyReductionMethod frequencyReductionMethod =
(StructurePlacement.FrequencyReductionMethod) declaredFieldValue(
StructurePlacement.class,
placement,
StructurePlacement.FrequencyReductionMethod.class);
float frequency = (float) declaredFieldValue(
StructurePlacement.class, placement, float.class);
int salt = (int) declaredFieldValue(
StructurePlacement.class, placement, int.class);
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
return new RandomSpreadStructurePlacement(
locateOffset,
frequencyReductionMethod,
scale.frequency(),
salt,
exclusionZone,
scale.spacing(),
randomSpread.separation(),
randomSpread.spreadType());
}
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
ConcentricRingsStructurePlacement rings =
(ConcentricRingsStructurePlacement) placement;
float scaledFrequency = NativeStructureFrequencyScale.probability(
frequency, frequencyMultiplier);
return new ConcentricRingsStructurePlacement(
locateOffset,
frequencyReductionMethod,
scaledFrequency,
salt,
exclusionZone,
rings.distance(),
rings.spread(),
rings.count(),
rings.preferredBiomes());
}
throw new IllegalStateException("Unsupported native structure placement in an affected "
+ "frequency-override or datapack-scope graph: "
+ placement.getClass().getName());
}
private static Object declaredFieldValue(
Class<?> declaringType,
Object target,
Class<?> fieldType
) {
Field match = null;
for (Field field : declaringType.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
continue;
}
if (match != null) {
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
+ declaringType.getName());
}
match = field;
}
if (match == null) {
throw new IllegalStateException("Missing " + fieldType.getName() + " field on "
+ declaringType.getName());
}
try {
match.setAccessible(true);
return match.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not read " + fieldType.getName() + " field on "
+ declaringType.getName(), e);
}
}
static final class StructureSetKeyIndex {
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries;
private StructureSetKeyIndex(
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries
) {
this.keysByEntries = keysByEntries;
}
private static StructureSetKeyIndex create(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
new IdentityHashMap<>();
for (Holder<StructureSet> holder : registeredStructureSets) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isEmpty()) {
continue;
}
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
if (previous != null && !previous.equals(key.get())) {
throw new IllegalStateException(
"Multiple registered structure sets share the same entry list");
}
}
return new StructureSetKeyIndex(keys);
}
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key;
}
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
}
}
record Selection(
List<Holder<StructureSet>> structureSets,
int retainedManagedSets,
int excludedManagedSets
) {
}
private static final class ReboundStructureSetHolder extends Holder.Reference<StructureSet> {
private ReboundStructureSetHolder(ResourceKey<StructureSet> key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, key, null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
}
@@ -0,0 +1,399 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedFeatureControl;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.FeatureSorter;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
* {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the
* control off nothing here allocates or runs.
*
* <p>Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false},
* so the vanilla feature half never runs on its own. This reproduces that half only - structures are never
* placed twice.
*
* <p>Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The
* FEATURES chunk step is not parallel-safe.
*/
final class ImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine;
private volatile FeatureTable featureTable;
private volatile IrisDimension inertDimension;
private volatile int settledRuntimeId;
ImportedFeatureStage(Engine engine) {
this.engine = engine;
}
/**
* Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative.
* Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it
* does today.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
FeatureTable table = featureTable;
if (table == null) {
return null;
}
Holder<Biome> mapped = table.derivatives().get(holderKey(biome));
return mapped == null ? null : mapped.value().getGenerationSettings();
}
/**
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
*
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
*/
void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (settled(dimension, runtimeId)) {
return;
}
synchronized (this) {
if (settled(dimension, runtimeId)) {
return;
}
build(level, dimension);
settledRuntimeId = runtimeId;
}
}
private boolean settled(IrisDimension dimension, int runtimeId) {
if (settledRuntimeId != runtimeId) {
return false;
}
FeatureTable current = featureTable;
if (current != null && current.dimension() == dimension) {
return true;
}
return inertDimension == dimension;
}
private void build(WorldGenLevel level, IrisDimension dimension) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error);
markInert(dimension);
return;
}
if (!control.shouldGenerateFeatures()) {
markInert(dimension);
return;
}
FeatureTable built;
try {
built = buildTable(level, control, dimension);
} catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error);
markInert(dimension);
return;
}
if (built == null) {
markInert(dimension);
return;
}
featureTable = built;
inertDimension = null;
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps");
}
private void markInert(IrisDimension dimension) {
featureTable = null;
inertDimension = dimension;
}
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
IrisDimension dimension) {
Registry<Biome> registry = level.registryAccess().lookupOrThrow(Registries.BIOME);
Set<String> visibleKeys = visibleBiomeKeys();
// Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and
// an unordered walk makes detection depend on JVM hash order.
List<Holder<Biome>> biomes = new ArrayList<>();
Map<String, Holder<Biome>> byKey = new HashMap<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (key != null && visibleKeys.contains(key)) {
biomes.add(reference);
byKey.put(key, reference);
}
});
if (biomes.isEmpty()) {
IrisLogging.error("Iris importedFeatures is on but " + dimensionKey()
+ " exposes no registered biomes; features off");
return null;
}
Map<String, Holder<Biome>> derivatives = customBiomeDerivatives(registry, byKey);
List<FeatureSorter.StepFeatureData> steps;
try {
steps = FeatureSorter.buildFeaturesPerStep(biomes,
(Holder<Biome> biome) -> settingsFor(biome, derivatives).features(), true);
} catch (IllegalStateException error) {
String message = error.getMessage();
if (message == null || !message.contains(CYCLE_MARKER)) {
throw error;
}
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": the registered placed features cannot be ordered. " + message
+ ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false.");
return null;
}
boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty();
return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes),
Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered);
}
/**
* Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every
* scatter entry, and every generated custom biome.
*/
private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addKey(keys, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addKey(keys, scatter);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, namespace + ":" + customBiome.getId());
}
}
return keys;
}
/**
* Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla
* derivative. The custom biome's own datapack JSON carries no features by design.
*/
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
if (!irisBiome.isCustom()) {
continue;
}
String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey());
// Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure
// derivative is rewritten away from its vanilla derivative, so the derivative whose features we
// want is not always a biome Iris can emit.
Holder<Biome> derivative = byKey.get(derivativeKey);
if (derivative == null) {
derivative = resolveHolder(registry, derivativeKey);
}
if (derivative == null) {
IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome "
+ irisBiome.getLoadKey()
+ " is not registered; its custom biomes generate no imported features");
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
}
}
return derivatives;
}
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
if (registry == null || key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
.orElse(null);
}
private static BiomeGenerationSettings settingsFor(Holder<Biome> biome,
Map<String, Holder<Biome>> derivatives) {
Holder<Biome> mapped = derivatives.get(holderKey(biome));
return mapped == null
? biome.value().getGenerationSettings()
: mapped.value().getGenerationSettings();
}
/**
* Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
FeatureTable table = featureTable;
if (table == null) {
return;
}
ChunkPos centerPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<PlacedFeature> featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue;
}
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex);
}
} catch (Throwable error) {
throw new IllegalStateException("Iris imported feature placement failed for chunk "
+ centerPos.x() + "," + centerPos.z(), error);
} finally {
level.setCurrentlyGenerating(null);
}
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
BlockPos origin, int stepIndex) {
IntSet stepFeatures = new IntArraySet();
for (Holder<Biome> biome : chunkBiomes) {
List<HolderSet<PlacedFeature>> biomeFeatures = settingsFor(biome, table.derivatives()).features();
if (stepIndex >= biomeFeatures.size()) {
continue;
}
for (Holder<PlacedFeature> feature : biomeFeatures.get(stepIndex)) {
stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value()));
}
}
if (stepFeatures.isEmpty()) {
return;
}
// Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own
// global index, so denying one feature never shifts another.
int[] featureIndices = stepFeatures.toIntArray();
Arrays.sort(featureIndices);
for (int globalIndex : featureIndices) {
PlacedFeature feature = stepData.features().get(globalIndex);
if (table.filtered()) {
Identifier featureId = featureRegistry.getKey(feature);
if (featureId != null && !table.control().shouldGenerate(featureId.toString())) {
continue;
}
}
random.setFeatureSeed(decorationSeed, globalIndex, stepIndex);
level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature));
feature.placeWithBiomeCheck(level, owner, random, origin);
}
}
private static String describeFeature(Registry<PlacedFeature> registry, PlacedFeature feature) {
Identifier id = registry.getKey(feature);
return id == null ? feature.toString() : id.toString();
}
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) {
section.getBiomes().getAll(collected::add);
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
for (Holder<Biome> biome : collected) {
if (table.biomeSet().contains(biome)) {
present.add(biome);
continue;
}
String key = holderKey(biome);
Holder<Biome> canonical = key == null ? null : table.byKey().get(key);
if (canonical != null) {
present.add(canonical);
}
}
return present;
}
private static void addKey(Set<String> keys, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
keys.add(normalized);
}
}
private String dimensionKey() {
IrisDimension dimension = engine.getDimension();
return dimension == null ? "<unbound>" : dimension.getLoadKey();
}
private static String holderKey(Holder<Biome> holder) {
return holder.unwrapKey()
.map(key -> key.identifier().toString().toLowerCase(Locale.ROOT))
.orElse(null);
}
private static String normalizeKey(String key) {
Identifier identifier = key == null ? null : Identifier.tryParse(key);
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control,
List<Holder<Biome>> biomes, Set<Holder<Biome>> biomeSet,
Map<String, Holder<Biome>> byKey,
List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) {
}
}
@@ -0,0 +1,581 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.level.chunk.EmptyLevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.bukkit.event.entity.CreatureSpawnEvent;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
import java.util.function.Supplier;
final class NativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private final WorldGenLevel delegate;
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
private final AABB generationBounds;
private final LevelTickAccess<Block> blockTicks;
private final LevelTickAccess<Fluid> fluidTicks;
private final Long2LongOpenHashMap terrainHeights;
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
this.generationCenter = Objects.requireNonNull(
boundary.generationCenter(), "Native structure world access requires a generation center");
this.surfaceFirstFreeY = Objects.requireNonNull(
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
this.generationBounds = new AABB(
generationCenter.getMinBlockX() - 16,
delegate.getMinY(),
generationCenter.getMinBlockZ() - 16,
generationCenter.getMaxBlockX() + 17,
delegate.getMinY() + delegate.getHeight(),
generationCenter.getMaxBlockZ() + 17);
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
this.terrainHeights = new Long2LongOpenHashMap();
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
this.outsideChunks = new Long2ObjectOpenHashMap<>();
}
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY));
}
@Override
public long getSeed() {
return delegate.getSeed();
}
@Override
public boolean ensureCanWrite(BlockPos position) {
return isWritable(position) && delegate.ensureCanWrite(position);
}
@Override
public void setCurrentlyGenerating(Supplier<String> description) {
delegate.setCurrentlyGenerating(description);
}
@Override
public ServerLevel getLevel() {
return delegate.getLevel();
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
if (isReadable(position)) {
return delegate.getCurrentDifficultyAt(position);
}
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
}
@Override
public long nextSubTickCount() {
return delegate.nextSubTickCount();
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return blockTicks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return fluidTicks;
}
@Override
public LevelData getLevelData() {
return delegate.getLevelData();
}
@Override
public MinecraftServer getServer() {
return delegate.getServer();
}
@Override
public ChunkSource getChunkSource() {
return delegate.getChunkSource();
}
@Override
public RandomSource getRandom() {
return delegate.getRandom();
}
@Override
public void updateNeighborsAt(BlockPos position, Block block) {
if (isWritableNeighbourhood(position)) {
delegate.updateNeighborsAt(position, block);
}
}
@Override
public void neighborShapeChanged(Direction direction, BlockPos position,
BlockPos neighbourPosition, BlockState neighbourState,
int updateFlags, int updateLimit) {
if (isWritable(position) && isWritable(neighbourPosition)) {
delegate.neighborShapeChanged(
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
}
}
@Override
public void playSound(Entity source, BlockPos position, SoundEvent sound,
SoundSource soundSource, float volume, float pitch) {
if (isWritable(position)) {
delegate.playSound(source, position, sound, soundSource, volume, pitch);
}
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z,
double velocityX, double velocityY, double velocityZ) {
if (isWritable(BlockPos.containing(x, y, z))) {
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
}
}
@Override
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
if (isWritable(position)) {
delegate.levelEvent(source, eventId, position, data);
}
}
@Override
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
if (isWritable(BlockPos.containing(position))) {
delegate.gameEvent(event, position, context);
}
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ, status, create);
}
return create ? outsideChunk(chunkX, chunkZ) : null;
}
@Override
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
if (!isInsideGenerationRegion(chunkX, chunkZ)) {
return null;
}
return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ);
}
@Override
public boolean hasChunk(int chunkX, int chunkZ) {
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
}
@Override
public int getSkyDarken() {
return delegate.getSkyDarken();
}
@Override
public BiomeManager getBiomeManager() {
return biomeManager;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
return getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
}
return fallbackBiome;
}
@Override
public boolean isClientSide() {
return delegate.isClientSide();
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public DimensionType dimensionType() {
return delegate.dimensionType();
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getHeight() {
return delegate.getHeight();
}
@Override
public RegistryAccess registryAccess() {
return delegate.registryAccess();
}
@Override
public FeatureFlagSet enabledFeatures() {
return delegate.enabledFeatures();
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
return delegate.environmentAttributes();
}
@Override
public LevelLightEngine getLightEngine() {
return delegate.getLightEngine();
}
@Override
public int getBrightness(LightLayer layer, BlockPos position) {
if (isReadable(position)) {
return delegate.getBrightness(layer, position);
}
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
}
@Override
public int getRawBrightness(BlockPos position, int ambientDarkening) {
if (isReadable(position)) {
return delegate.getRawBrightness(position, ambientDarkening);
}
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
}
@Override
public boolean canSeeSky(BlockPos position) {
if (isReadable(position)) {
return delegate.canSeeSky(position);
}
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
}
@Override
public WorldBorder getWorldBorder() {
return delegate.getWorldBorder();
}
@Override
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunkForCollisions(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
public BlockState getBlockState(BlockPos position) {
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null;
}
@Override
public FluidState getFluidState(BlockPos position) {
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getFluidIfLoaded(position) : null;
}
@Override
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
}
@Override
public <T extends Entity> List<T> getEntities(
EntityTypeTest<Entity, T> type,
AABB area, Predicate<? super T> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
}
@Override
public List<? extends Player> players() {
return delegate.players();
}
@Override
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
return predicate.test(getBlockState(position));
}
@Override
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
return predicate.test(getFluidState(position));
}
@Override
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
return position.atY(getHeight(type, position.getX(), position.getZ()));
}
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
return isWritable(position) && delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
public boolean addFreshEntity(Entity entity) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
}
@Override
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason);
}
private boolean isReadable(BlockPos position) {
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
&& !isOutsideBuildHeight(position);
}
private boolean isWritable(BlockPos position) {
return isReadable(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
return isWritable(position)
&& isWritable(position.north())
&& isWritable(position.south())
&& isWritable(position.east())
&& isWritable(position.west())
&& isWritable(position.above())
&& isWritable(position.below());
}
private AABB boundedArea(AABB area) {
double minX = Math.max(area.minX, generationBounds.minX);
double minY = Math.max(area.minY, generationBounds.minY);
double minZ = Math.max(area.minZ, generationBounds.minZ);
double maxX = Math.min(area.maxX, generationBounds.maxX);
double maxY = Math.min(area.maxY, generationBounds.maxY);
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
return null;
}
if (minX == area.minX && minY == area.minY && minZ == area.minZ
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
return area;
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
}
@Override
public boolean isOutsideBuildHeight(BlockPos position) {
return position.getY() < getMinY() || position.getY() >= getMaxY();
}
private BlockState terrainState(BlockPos position) {
if (isOutsideBuildHeight(position)) {
return Blocks.VOID_AIR.defaultBlockState();
}
long heights = heights(position.getX(), position.getZ());
int surface = (int) (heights >> 32);
int floor = (int) heights;
if (position.getY() < floor) {
return Blocks.STONE.defaultBlockState();
}
if (position.getY() < surface) {
return Blocks.WATER.defaultBlockState();
}
return Blocks.AIR.defaultBlockState();
}
private int height(Heightmap.Types type, int x, int z) {
long heights = heights(x, z);
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
return (int) heights;
}
return (int) (heights >> 32);
}
private long heights(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
long cached = terrainHeights.get(key);
if (cached != Long.MIN_VALUE) {
return cached;
}
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
terrainHeights.put(key, heights);
return heights;
}
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
long key = ChunkPos.pack(chunkX, chunkZ);
ChunkAccess cached = outsideChunks.get(key);
if (cached != null) {
return cached;
}
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
outsideChunks.put(key, chunk);
return chunk;
}
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
private final LevelTickAccess<T> delegate;
private final Predicate<BlockPos> writable;
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
this.delegate = delegate;
this.writable = writable;
}
@Override
public void schedule(ScheduledTick<T> tick) {
if (writable.test(tick.pos())) {
delegate.schedule(tick);
}
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return writable.test(position) && delegate.hasScheduledTick(position, type);
}
@Override
public int count() {
return delegate.count();
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return writable.test(position) && delegate.willTickThisTick(position, type);
}
}
}
@@ -0,0 +1,80 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
final class VanillaStructureBiomes {
private VanillaStructureBiomes() {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
if (source == null) {
throw new IllegalStateException("Minecraft chunk generator has no biome source");
}
Set<String> keys = new LinkedHashSet<>();
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
? customBiomeSource.possibleStructureBiomes()
: source.possibleBiomes();
for (Holder<Biome> holder : possibleBiomes) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
if (structureKey == null || structureKey.isBlank()) {
throw new IllegalArgumentException("Registered structure key must not be blank");
}
Set<String> keys = new LinkedHashSet<>();
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
boolean hasFilterEntries = false;
for (Holder<Biome> holder : structure.biomes()) {
hasFilterEntries = true;
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
// An empty biome filter is legal datapack content (opt-in structures whose tags only
// reference absent modded biomes); the structure is unreachable, not an error state.
if (keys.isEmpty() && hasFilterEntries) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' has biome filter entries but none resolve to registered biome keys");
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
if (level == null) {
throw new IllegalStateException("Minecraft server level is unavailable");
}
Set<String> possible = possibleBiomeKeys(source);
return NativeStructureGenerationKeys.reachable(level, possible);
}
}
@@ -0,0 +1,70 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest {
@Test
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("resolveBiomeHolder(registry, i.getStructureDerivativeKey())"));
assertTrue(source.contains("resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey())"));
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
}
@Test
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(source.contains("if (engine.isClosed())"));
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()"));
}
@Test
public void strongholdRingSearchSamplesOneQuartColumnPerChunk() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
assertTrue(source.contains(
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
}
@Test
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int lookupStart = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(");
int lookupEnd = source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", lookupStart);
assertTrue(lookupStart >= 0);
assertTrue(lookupEnd > lookupStart);
String lookup = source.substring(lookupStart, lookupEnd);
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
}
}
@@ -0,0 +1,551 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
String locate = source.substring(locateStart, locateEnd);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
String reachability = source.substring(reachabilityStart, reachabilityEnd);
int bootstrapGate = locate.indexOf("!platformGenerator.shouldGenerateStructures()");
int generatorIdentity = locate.indexOf("level.getChunkSource().getGenerator() != this");
int lease = locate.indexOf("requireGenerationLease(\"bukkit_nms_structure_locate\")");
int nativePrediction = locate.indexOf("NativeStructureVanillaLocator.predict(");
assertTrue(locate.contains("reached its safety limit"));
assertTrue(locate.contains("unregistered structure holder"));
assertTrue(bootstrapGate >= 0);
assertTrue(generatorIdentity > bootstrapGate);
assertTrue(lease > generatorIdentity);
assertTrue(nativePrediction > lease);
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("IrisLogging.reportError"));
assertFalse(reachability.contains("catch (Throwable"));
assertFalse(reachability.contains("reachable = Set.of()"));
}
@Test
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
String placement = source.substring(placementStart, placementEnd);
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
assertTrue(adjustment.contains("\"vertical adjustment\""));
assertFalse(adjustment.contains("IrisLogging.reportError"));
assertTrue(placement.contains("\"resolution\""));
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"terrain preparation\""));
assertTrue(placement.contains("\"foundation repair\""));
assertFalse(placement.contains("\"terrain carving\""));
assertTrue(placement.contains("\"vegetation cleanup\""));
assertTrue(placement.contains("\"placement\""));
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
@Test
public void heightmapRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_heightmaps\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_height\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
}
@Test
public void structureReferenceRepairIsGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int referencesStart = source.indexOf("public void createReferences");
int referencesEnd = source.indexOf("public CompletableFuture<ChunkAccess> createBiomes", referencesStart);
String references = source.substring(referencesStart, referencesEnd);
int nativeStructureGate = references.indexOf("!platformGenerator.shouldGenerateStructures()");
int generatorIdentity = references.indexOf("runtimeLevel.getChunkSource().getGenerator() != this");
int stage = references.indexOf("requireGenerationStage(\"bukkit_nms_create_references\")");
int lease = references.indexOf("requireGenerationLease(\"bukkit_nms_create_references\")");
int repair = references.indexOf("NativeStructureReferenceRepair.createReferences(");
assertTrue(nativeStructureGate >= 0);
assertTrue(generatorIdentity > nativeStructureGate);
assertTrue(stage > generatorIdentity);
assertTrue(lease > stage);
assertTrue(repair > lease);
assertTrue(references.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertFalse(references.contains("delegate.createReferences("));
assertFalse(references.contains("catch ("));
}
@Test
public void structureGenerationAcquiresBootstrapGateBeforeUsingPublishedState() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int generationStart = source.indexOf("public void createStructures(");
int generationEnd = source.indexOf("private void adjustGeneratedStructures", generationStart);
String generation = source.substring(generationStart, generationEnd);
int gate = generation.indexOf("!platformGenerator.shouldGenerateStructures()");
int currentGenerator = generation.indexOf(
"runtimeLevel.getChunkSource().getGenerator() != this");
int stateIdentity = generation.indexOf(
"runtimeLevel.getChunkSource().getGeneratorState() != structureState");
int stage = generation.indexOf(
"requireGenerationStage(\"bukkit_nms_create_structures\")");
int lease = generation.indexOf(
"requireGenerationLease(\"bukkit_nms_create_structures\")");
int generationCall = generation.indexOf(
"super.createStructures(registryAccess, structureState");
assertTrue(gate >= 0);
assertTrue(currentGenerator > gate);
assertTrue(stateIdentity > currentGenerator);
assertTrue(stage > stateIdentity);
assertTrue(lease > stage);
assertTrue(generationCall > lease);
assertTrue(generation.contains("structureState,\n structureManager"));
assertFalse(generation.contains("resolvePublishedStructureState"));
}
@Test
public void nativeRingBootstrapTracksTheExactMinecraftFutures() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int initializeStart = source.indexOf("private CompletableFuture<Void> initializeStructureState(");
int initializeEnd = source.indexOf("private Map<?, ?> structureRingPositions", initializeStart);
String initialize = source.substring(initializeStart, initializeEnd);
int completionStart = source.indexOf(
"private CompletableFuture<Void> structureRingCompletion", initializeEnd);
int completionEnd = source.indexOf("private void requireCurrentStructureOwner", completionStart);
String completion = source.substring(completionStart, completionEnd);
assertBefore(initialize,
"structureRingPositions(structureState)",
"structureState.ensureStructuresGenerated()");
assertBefore(initialize,
"structureState.ensureStructuresGenerated()",
"structureRingCompletion(ringPositions)");
assertFalse(initialize.contains("catch ("));
assertTrue(completion.contains("for (Object candidate : ringPositions.values())"));
assertTrue(completion.contains("CompletableFuture.allOf(completions)"));
assertTrue(completion.contains("throw new IllegalStateException("));
assertFalse(completion.contains("join()"));
assertFalse(completion.contains("get()"));
}
@Test
public void standardActivationClaimsTheExactPublishedStateBeforeInitialization() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int activationStart = source.indexOf("CompletableFuture<Void> activateStudioStructureState(");
int activationEnd = source.indexOf(
"private CompletableFuture<Void> startStructureStateBootstrap", activationStart);
String activation = source.substring(activationStart, activationEnd);
int bootstrapStart = activationEnd;
int bootstrapEnd = source.indexOf(
"private CompletableFuture<Void> initializeStructureState", bootstrapStart);
String bootstrap = source.substring(bootstrapStart, bootstrapEnd);
assertTrue(activation.contains("retained.structureState()"));
assertBefore(activation,
"retainedStudioStructureState(",
"claimStudioStructureState(retained)");
assertBefore(bootstrap,
"claim,",
"initializeStructureState(");
assertBefore(bootstrap,
"initializeStructureState(",
"activation.run()");
assertFalse(activation.contains("publishStructureState("));
assertFalse(source.contains("ChunkGeneratorStructureState fullState"));
}
@Test
public void terrainWritesPrimeTheWorldgenHeightmapsForEveryChunk() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
int fillEnd = source.indexOf("public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt", fillStart);
String fill = source.substring(fillStart, fillEnd);
int decorationStart = source.indexOf("public void addVanillaDecorations");
int decorationEnd = source.indexOf("public void spawnOriginalMobs", decorationStart);
String decorations = source.substring(decorationStart, decorationEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(fill.contains("thenApply"));
assertTrue(fill.contains("primeWorldgenHeightmaps"));
assertTrue(decorations.contains("primeWorldgenHeightmaps"));
assertFalse(decorations.contains("Heightmap.Types.WORLD_SURFACE_WG"));
assertFalse(decorations.contains("Heightmap.Types.OCEAN_FLOOR_WG"));
assertEquals(1, occurrences(source, "WorldgenTerrainHeightmaps.primeTerrain("));
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(fill.contains("requireGenerationLease(\"bukkit_nms_chunk_pipeline\")"));
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@Test
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
.replace("\r\n", "\n");
String createBiomes = method(
source,
"public CompletableFuture<ChunkAccess> createBiomes",
"public void buildSurface");
String buildSurface = method(
source,
"public void buildSurface",
"public void applyCarvers");
String carvers = method(
source,
"public void applyCarvers",
"public CompletableFuture<ChunkAccess> fillFromNoise");
String noise = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
String decoration = method(
source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
String spawning = method(
source,
"public void spawnOriginalMobs",
"private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables");
String baseHeight = method(
source,
"public int getBaseHeight",
"public NoiseColumn getBaseColumn");
String baseColumn = method(
source,
"public NoiseColumn getBaseColumn",
"private GenerationSessionLease requireGenerationLease");
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
assertTrue(createBiomes.contains("sampler, resolverState"));
assertTrue(buildSurface.contains("delegate.buildSurface("));
assertTrue(carvers.contains("delegate.applyCarvers("));
assertTrue(noise.contains("requireNoiseGenerationStage("));
assertTrue(noise.contains("\"bukkit_nms_chunk_pipeline\""));
assertTrue(noise.contains("stage.close()"));
assertTrue(noise.contains("completion.cancel(false)"));
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
assertTrue(spawning.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(baseHeight.contains("engine.getHeight("));
assertTrue(baseColumn.contains("engine.getHeight("));
assertFalse(source.contains("synthetic_entry"));
assertFalse(source.contains("StudioEntryChunkGenerator"));
}
@Test
public void fillFromNoiseLeaseSpansTheDelegateAndHeightmapPipeline() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
int fillEnd = source.indexOf("private static boolean isCancellationFailure", fillStart);
String fill = source.substring(fillStart, fillEnd);
int productionStart = fill.indexOf(
" BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
0);
assertTrue(productionStart >= 0);
String productionFill = fill.substring(productionStart);
int completionStart = productionFill.indexOf("pipeline.whenComplete(");
int completionEnd = productionFill.indexOf(" return completion;", completionStart);
assertTrue(completionStart >= 0);
assertTrue(completionEnd > completionStart);
String completion = productionFill.substring(completionStart, completionEnd);
assertBefore(productionFill,
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
"GenerationSessionLease lease");
assertBefore(productionFill,
"requireNoiseGenerationStage(",
"\"bukkit_nms_chunk_pipeline\")");
assertBefore(productionFill,
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
assertBefore(completion, "lease.close();", "stage.close();");
assertBefore(completion, "stage.close();", "completion.complete(filled)");
assertBefore(completion, "stage.close();", "completion.cancel(false)");
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
assertTrue(completion.contains("else if (cancelled)"));
assertFalse(completion.contains("pipeline.isCancelled()"));
assertFalse(completion.contains("finally"));
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
assertTrue(productionFill.contains("return completion;"));
assertFalse(productionFill.contains("return pipeline;"));
assertFalse(productionFill.contains("pipeline.cancel("));
assertFalse(productionFill.contains("bukkit_nms_worldgen_heightmaps"));
}
@Test
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
Semaphore admission = new Semaphore(1, true);
admission.acquire();
CountDownLatch exclusiveEntered = new CountDownLatch(1);
CountDownLatch releaseExclusive = new CountDownLatch(1);
AtomicBoolean dependentFinished = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
boolean stageReleased = false;
try {
Future<?> exclusive = executor.submit(() -> {
boolean acquired = false;
try {
admission.acquire();
acquired = true;
exclusiveEntered.countDown();
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} finally {
if (acquired) {
admission.release();
}
}
});
awaitQueueLength(admission, 1);
CompletableFuture<Void> outward = new CompletableFuture<>();
outward.thenRun(() -> {
try {
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
dependentFinished.set(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
});
admission.release();
stageReleased = true;
assertTrue(outward.complete(null));
assertTrue(dependentFinished.get());
releaseExclusive.countDown();
exclusive.get(2, TimeUnit.SECONDS);
assertEquals(1, admission.availablePermits());
} finally {
if (!stageReleased) {
admission.release();
}
releaseExclusive.countDown();
executor.shutdownNow();
}
}
@Test
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() throws IOException {
CompletableFuture<Void> delegate = new CompletableFuture<>();
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
assertTrue(delegate.cancel(false));
assertFalse(transformed.isCancelled());
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
assertTrue(failure.getCause() instanceof CancellationException);
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String cancellation = method(
source,
"private static boolean isCancellationFailure",
"private void primeWorldgenHeightmaps");
assertTrue(cancellation.contains("current instanceof CompletionException"));
assertTrue(cancellation.contains("current instanceof CancellationException"));
}
@Test
public void noiseAdmissionCanPrepareStudioBeforeAcquiringTheGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String fill = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
String admission = method(
source,
"private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage",
"public Optional<Identifier> getTypeNameForDataFixer");
assertBefore(fill,
"requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
assertTrue(admission.contains("platformGenerator.acquireNoiseGenerationStage("));
assertBefore(admission, "engine,", "chunkPos.x(),");
assertBefore(admission, "chunkPos.x(),", "chunkPos.z(),");
assertFalse(fill.contains("requireGenerationStage(\"bukkit_nms_chunk_pipeline\")"));
}
@Test
public void everyTopLevelMoonriseStageEntersTheFairGateBeforeItsGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
String decoration = method(source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
assertStageBeforeLease(references, "bukkit_nms_create_references");
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
assertBefore(noise,
"requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
assertStageBeforeLease(decoration, "bukkit_nms_biome_decoration");
assertBefore(decoration,
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
"importedFeatures.prepare(generatoraccessseed)");
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
assertFalse(source.contains("GenerationSessionManager"));
}
@Test
public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java");
assertTrue("Worldgen heightmap priming must be shared with the modded loaders through "
+ nativegen, Files.isRegularFile(shared));
String heightmaps = Files.readString(shared).replace("\r\n", "\n");
assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;"));
assertTrue(heightmaps.contains("public static void primeTerrain("));
assertTrue(heightmaps.contains("public static void primeStructurePlacement("));
assertFalse(heightmaps.contains("org.bukkit"));
assertFalse(heightmaps.contains("craftbukkit"));
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
}
@Test
public void onlySidecarOwnedInjectedStartsUsePersistedPlacementPolicy() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
int ownershipResolution = placement.indexOf(
"NativeStructureOwnershipRecovery.resolve(");
int persistedDecision = placement.indexOf("ownership.restoredDecision()", ownershipResolution);
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
String factory = Files.readString(nativegen.resolve("NativeStructureFactory.java")).replace("\r\n", "\n");
String injector = Files.readString(nativegen.resolve("NativeStructureStartInjector.java")).replace("\r\n", "\n");
String recovery = Files.readString(nativegen.resolve("NativeStructureOwnershipRecovery.java")).replace("\r\n", "\n");
int ownershipRecord = injector.indexOf("NativeStructureOwnershipStore.record(");
int startPublication = injector.indexOf(
"context.structureManager().setStartForStructure(", ownershipRecord);
assertTrue(ownershipResolution >= 0);
assertTrue(persistedDecision > ownershipResolution);
assertTrue(recovery.contains("NativeStructureOwnershipStore.findPersisted("));
assertTrue(recovery.contains("NativeStructureOwnershipStore.record("));
assertTrue(ownershipRecord >= 0);
assertTrue(startPublication > ownershipRecord);
assertTrue(factory.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
assertTrue(adjustment.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
assertFalse(source.contains("wrapManaged("));
assertFalse(source.contains("isIrisManagedStart("));
}
private static int occurrences(String source, String needle) {
int count = 0;
int index = source.indexOf(needle);
while (index >= 0) {
count++;
index = source.indexOf(needle, index + needle.length());
}
return count;
}
private static void awaitQueueLength(Semaphore semaphore, int expected) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertTrue("Expected at least " + expected + " queued semaphore threads",
semaphore.getQueueLength() >= expected);
}
private static String method(String source, String startToken, String endToken) {
int start = source.indexOf(startToken);
int end = source.indexOf(endToken, start);
assertTrue("Missing source contract token: " + startToken, start >= 0);
assertTrue("Missing source contract token: " + endToken, end > start);
return source.substring(start, end);
}
private static void assertStageBeforeLease(String source, String operation) {
assertBefore(source,
"requireGenerationStage(\"" + operation + "\")",
"requireGenerationLease(\"" + operation + "\")");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
@Test
public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int spawnStart = source.indexOf("public void spawnOriginalMobs");
int spawnEnd = source.indexOf("private static WeightedList", spawnStart);
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))"));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
}
}
@@ -0,0 +1,190 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void nativeLocateAllowsMonumentsAfterPolicyAndReachabilityChecks() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
assertTrue(irisHelperStart > findStart);
assertTrue(filterStart > irisHelperStart);
String outerMethod = source.substring(findStart, irisHelperStart);
String irisHelper = source.substring(irisHelperStart, filterStart);
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int placedCheck = irisHelper.indexOf(
"if (!IrisStructureLocator.hasNativePlacement(engine, structureId))");
int irisLocate = irisHelper.indexOf("NativeStructureLocatePersistence.search(", placedCheck);
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
int nativePrediction = outerMethod.indexOf("NativeStructureVanillaLocator.predict(", nativeFilter);
int irisResolution = outerMethod.indexOf("return findNearestIrisStructure(", nativePrediction);
int nearestSelection = irisHelper.indexOf(
"NativeStructureLocateResults.nearest(pos, predicted, nativeLocated)");
int selectedVerification = irisHelper.indexOf("bestSearch.search().verify(bestResult)", nearestSelection);
int selectedReference = irisHelper.indexOf(
"selectedSearch.search().reference(selectedStart)", selectedVerification);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int policyFilter = filterMethod.indexOf("if (!decision.generate())");
int filterContinue = filterMethod.indexOf("continue;", policyFilter);
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue);
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
assertTrue(registryLookup >= 0);
assertTrue(placedCheck > registryLookup);
assertTrue(irisLocate > placedCheck);
assertTrue(searchLimit > irisLocate);
assertTrue(nativeFilter >= 0);
assertTrue(nativePrediction > nativeFilter);
assertTrue(irisResolution > nativePrediction);
assertTrue(nearestSelection > irisLocate);
assertTrue(selectedVerification > nearestSelection);
assertTrue(selectedReference > selectedVerification);
assertTrue(policyFilter >= 0);
assertTrue(filterContinue > policyFilter);
assertTrue(emptyNativePartition > filterContinue);
assertTrue(reachabilityLookup > emptyNativePartition);
assertFalse(filterMethod.contains("NativeStructureLocateCapability"));
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
assertTrue(irisHelper.contains("NativeStructureLocatePersistence.probe("));
assertTrue(irisHelper.contains("NativeStructureLocateResults.selectAndReference("));
assertTrue(irisHelper.contains("selectedSearch.search().reference(selectedStart)"));
assertTrue(irisHelper.contains("findUnexplored"));
assertTrue(irisHelper.contains("verified.ownership().locatorY()"));
assertFalse(outerMethod.contains("delegate.findNearestMapStructure("));
}
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
@Test
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
AtomicInteger irisReferences = new AtomicInteger();
AtomicInteger nativeReferences = new AtomicInteger();
Pair<BlockPos, Holder<Structure>> irisSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeFar, () -> nativeReferences.incrementAndGet());
assertSame(irisNear, irisSelected);
assertEquals(1, irisReferences.get());
assertEquals(0, nativeReferences.get());
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeNear, () -> nativeReferences.incrementAndGet());
assertSame(nativeNear, nativeSelected);
assertEquals(1, irisReferences.get());
assertEquals(1, nativeReferences.get());
}
@Test
public void nativePredictionIsReadOnlyUntilTheWinnerIsCommitted() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
String source = Files.readString(nativegen.resolve("NativeStructureVanillaLocator.java")).replace("\r\n", "\n");
int predictionStart = source.indexOf("private static Candidate predictAt(");
int candidateStart = source.indexOf("public static final class Candidate", predictionStart);
String prediction = source.substring(predictionStart, candidateStart);
String candidate = source.substring(candidateStart);
assertFalse(prediction.contains("addReference("));
assertTrue(candidate.contains("structureManager.addReference(referenceStart)"));
assertTrue(candidate.contains("committed.compareAndSet(false, true)"));
}
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"));
String source = Files.readString(processor).replace("\r\n", "\n");
String foundation = Files.readString(
processor.resolveSibling("NativeStructureFoundationBuilder.java")).replace("\r\n", "\n");
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int stiltDefinition = foundation.indexOf("static void placeStilts(");
int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition);
int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(stiltDefinition >= 0);
assertTrue(occupancyCheck > stiltDefinition);
assertTrue(terrainFloor > stiltDefinition);
assertFalse(source.contains("state.equals("));
assertFalse(foundation.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
assertFalse(foundation.contains("snapshot.states"));
}
@Test
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))
.resolveSibling("NativeStructureVerticalPlacer.java")).replace("\r\n", "\n");
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
assertTrue(placementStart >= 0);
assertTrue(shiftStart > placementStart);
assertTrue(alignmentStart > shiftStart);
assertTrue(moveStart > alignmentStart);
assertTrue(movePieceStart > moveStart);
assertTrue(monumentChildrenStart > movePieceStart);
String placementMethod = source.substring(placementStart, shiftStart);
String shiftMethod = source.substring(shiftStart, alignmentStart);
String moveMethod = source.substring(moveStart, movePieceStart);
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
assertTrue(placementMethod.contains("return applyVerticalShift("));
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
}
}
@@ -0,0 +1,609 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisStructureSetFrequencyOverride;
import art.arcane.volmlib.util.collection.KList;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import org.junit.Test;
import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NMSBindingDatapackStructureScopeTest {
private static final String SOURCE = "https://example.test/managed.zip";
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:pillager_outpost");
Holder<StructureSet> managedSet = structureSetHolder(
"managed:illager_barracks", vanillaStructure);
DatapackStructureScopeIndex index = index(
List.of(),
List.of("managed:illager_barracks"));
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
List.of(managedSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(0, vanilla.structureSets().size());
assertEquals(1, vanilla.excludedManagedSets());
assertEquals(1, declaring.structureSets().size());
assertSame(managedSet, declaring.structureSets().getFirst());
}
@Test
public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:village_plains");
Holder<Structure> managedStructure = structureHolder("managed:tavern");
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:villages", vanillaStructure, managedStructure);
DatapackStructureScopeIndex index = index(
List.of("managed:tavern"),
List.of());
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
List.of(vanillaSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(1, vanilla.structureSets().size());
assertEquals(1, vanilla.structureSets().getFirst().value().structures().size());
assertSame(vanillaStructure,
vanilla.structureSets().getFirst().value().structures().getFirst().structure());
assertSame(vanillaSet.value().placement(),
vanilla.structureSets().getFirst().value().placement());
assertSame(vanillaSet, declaring.structureSets().getFirst());
}
@Test
public void setWithNoAllowedDefinitionsIsRemoved() {
Holder<StructureSet> unmanagedSet = structureSetHolder(
"minecraft:custom", structureHolder("managed:only"));
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(unmanagedSet), index, Set.of());
assertEquals(0, selection.structureSets().size());
}
@Test
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
ResourceKey<StructureSet> key = ResourceKey.create(
Registries.STRUCTURE_SET,
Identifier.parse("managed:illager_barracks"));
List<StructureSet.StructureSelectionEntry> entries = List.of(
new StructureSet.StructureSelectionEntry(
structureHolder("minecraft:pillager_outpost"), 1));
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 10387312)));
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 14357620)));
DatapackStructureScopeIndex scopeIndex = index(
List.of(), List.of("managed:illager_barracks"));
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
DatapackStructureStateFilter.keyIndex(List.of(registered));
DatapackStructureStateFilter.Selection excluded =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
Set.of(),
new IrisImportedStructureControl(),
keyIndex);
DatapackStructureStateFilter.Selection retained =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
scopeIndex.declaredSources(List.of(SOURCE)),
new IrisImportedStructureControl(),
keyIndex);
assertEquals(0, excluded.structureSets().size());
assertEquals(1, excluded.excludedManagedSets());
assertSame(spigotDirect, retained.structureSets().getFirst());
assertEquals(1, retained.retainedManagedSets());
}
@Test
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
.getResourceAsStream("DatapackStructureStateFilter.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
}
}
@Test
public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() {
Holder<StructureSet> complexes = structureSetHolder(
"minecraft:nether_complexes",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("minecraft:fortress"));
Holder<StructureSet> fossils = structureSetHolder(
"minecraft:nether_fossils",
new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921),
structureHolder("minecraft:nether_fossil"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
RandomSpreadStructurePlacement scaledComplexes =
(RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement();
RandomSpreadStructurePlacement unchangedFossils =
(RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement();
assertEquals(26, scaledComplexes.spacing());
assertEquals(4, scaledComplexes.separation());
assertEquals(2, unchangedFossils.spacing());
assertSame(fossils, selection.structureSets().get(1));
assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() {
Holder<StructureSet> exclusionTarget = structureSetHolder(
"example:target", structureHolder("example:target"));
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new UnsupportedPlacement(Optional.of(
new StructurePlacement.ExclusionZone(exclusionTarget, 1))),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
assertSame(custom, selection.structureSets().getFirst());
}
@Test
public void frequencyOnlyPassRebindsDependentExclusionTarget() {
Holder<StructureSet> target = structureSetHolder(
"example:target",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("example:target"));
RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new StructurePlacement.ExclusionZone(target, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> dependent = structureSetHolder(
"example:dependent",
dependentPlacement,
structureHolder("example:dependent"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:target")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
assertNotSame(dependent, scaledDependent);
assertNotSame(target, scaledTarget);
assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone(
scaledDependent.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledTarget.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassRebindsAffectedExclusionCycle() {
MutableStructureSetHolder first = new MutableStructureSetHolder("example:first");
MutableStructureSetHolder second = new MutableStructureSetHolder("example:second");
first.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
101,
Optional.of(new StructurePlacement.ExclusionZone(second, 1)),
32,
8,
RandomSpreadType.LINEAR),
"example:first"));
second.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
102,
Optional.of(new StructurePlacement.ExclusionZone(first, 1)),
27,
4,
RandomSpreadType.LINEAR),
"example:second"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:second")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone(
scaledFirst.value().placement()).orElseThrow().otherSet());
assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone(
scaledSecond.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledSecond.value().placement()).spacing());
}
@Test
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new CustomRandomSpreadPlacement(),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:custom")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
}
@Test
public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() {
Holder<StructureSet> managedSet = structureSetHolder(
"managed:blocked",
structureHolder("managed:blocked"));
RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new StructurePlacement.ExclusionZone(managedSet, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:allowed",
originalPlacement,
structureHolder("minecraft:village_plains"));
DatapackStructureScopeIndex index = index(
List.of("managed:blocked"),
List.of("managed:blocked"));
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(vanillaSet, managedSet), index, Set.of());
assertEquals(1, selection.structureSets().size());
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
}
@Test
public void standardPublishesOneDeferredStateWhileJigsawPublishesInitializedEmptyState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
int methodEnd = source.indexOf(
"\n @Override\n public CompletableFuture<Void> completeStudioStructureBootstrap",
methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
int jigsawMode = method.indexOf(
"boolean jigsawStudio = platformGenerator != null");
int jigsawOnly = method.indexOf("if (jigsawStudio)");
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
int emptyPublication = method.indexOf("stateField.set(chunkMap, bootstrapState);");
int standardOnly = method.indexOf("else if (studioBootstrap)");
int retention = method.indexOf("irisGenerator.retainStudioStructureState(");
int standardPublication = method.indexOf("stateField.set(chunkMap, scopedState);");
int immediateInitialization = method.indexOf("initializeAndPublishStructureState(");
assertTrue(filteredState >= 0);
assertTrue(jigsawMode > filteredState);
assertTrue(jigsawOnly > jigsawMode);
assertTrue(emptyCreation > jigsawOnly);
assertTrue(emptyFiltering > emptyCreation);
assertTrue(emptyInitialization > emptyFiltering);
assertTrue(emptyPublication > emptyInitialization);
assertTrue(standardOnly > emptyPublication);
assertTrue(retention > standardOnly);
assertTrue(standardPublication > retention);
assertTrue(immediateInitialization > standardPublication);
assertFalse(method.contains("scopedState.ensureStructuresGenerated();"));
assertFalse(method.contains("if (studioBootstrap && platformGenerator.isJigsawStudioActive())"));
}
@Test
public void standardCompletionReturnsTheExactActivationFutureWithoutReplacingState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public CompletableFuture<Void> completeStudioStructureBootstrap(World world)");
int methodEnd = source.indexOf("\n @Override\n public void abandonStudioStructureBootstrap", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int retained = method.indexOf("generator.retainedStudioStructureState(level, chunkMap)");
int emptyCompletion = method.indexOf("return CompletableFuture.completedFuture(null);");
int activation = method.indexOf("return generator.activateStudioStructureState(retained);");
assertTrue(retained >= 0);
assertTrue(emptyCompletion > retained);
assertTrue(activation > retained);
assertFalse(method.contains("stateField.set("));
assertFalse(method.contains("retained.fullState()"));
}
@Test
public void injectionVerifiesTheCanonicalPaperGeneratorBeforeStructureRetargeting() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public void inject(long seed, Engine engine, World world)");
int methodEnd = source.indexOf("\n @Override\n public DatapackStructureScopeResult", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int publication = method.indexOf("worldGenContextField.set(chunkMap, newContext);");
int canonicalRead = method.indexOf("level.getChunkSource().getGenerator()", publication);
int identityGate = method.indexOf("activeGenerator != irisGenerator", canonicalRead);
int structureRetarget = method.indexOf("retargetStructureCheck(level, irisGenerator)", identityGate);
assertTrue(publication >= 0);
assertTrue(canonicalRead > publication);
assertTrue(identityGate > canonicalRead);
assertTrue(structureRetarget > identityGate);
}
@Test
public void structureStateRecreationUsesTheWorldOwnedSpigotConfiguration() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("private ChunkGeneratorStructureState createStructureState(");
int methodEnd = source.indexOf(
"\n private void initializeAndPublishStructureState(", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("level.spigotConfig"));
assertFalse(method.contains("currentState.conf"));
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
) {
return DatapackStructureScopeIndex.create(List.of(
new DatapackIngestService.StructureScopeResources(
SOURCE,
structureKeys,
structureSetKeys)));
}
private static Holder<Structure> structureHolder(String key) {
return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null);
}
private static Holder<StructureSet> structureSetHolder(
String key,
Holder<Structure>... structures
) {
return structureSetHolder(
key,
new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345),
structures);
}
private static Holder<StructureSet> structureSetHolder(
String key,
StructurePlacement placement,
Holder<Structure>... structures
) {
List<StructureSet.StructureSelectionEntry> entries = Stream.of(structures)
.map(structure -> new StructureSet.StructureSelectionEntry(structure, 1))
.toList();
StructureSet value = new StructureSet(entries, placement);
return new KeyedHolder<>(
ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)),
value);
}
private static StructureSet structureSet(StructurePlacement placement, String structureKey) {
return new StructureSet(
List.of(new StructureSet.StructureSelectionEntry(
structureHolder(structureKey), 1)),
placement);
}
private static final class KeyedHolder<T> implements Holder<T> {
private final ResourceKey<T> key;
private final T value;
private KeyedHolder(ResourceKey<T> key, T value) {
this.key = key;
this.value = value;
}
@Override
public T value() {
return value;
}
@Override
public boolean isBound() {
return true;
}
@Override
public boolean areComponentsBound() {
return true;
}
@Override
public boolean is(Identifier identifier) {
return key.identifier().equals(identifier);
}
@Override
public boolean is(ResourceKey<T> candidate) {
return key.equals(candidate);
}
@Override
public boolean is(Predicate<ResourceKey<T>> predicate) {
return predicate.test(key);
}
@Override
public boolean is(TagKey<T> tag) {
return false;
}
@Override
public boolean is(Holder<T> holder) {
return holder == this;
}
@Override
public Stream<TagKey<T>> tags() {
return Stream.empty();
}
@Override
public DataComponentMap components() {
return DataComponentMap.EMPTY;
}
@Override
public Either<ResourceKey<T>, T> unwrap() {
return Either.left(key);
}
@Override
public Optional<ResourceKey<T>> unwrapKey() {
return Optional.of(key);
}
@Override
public Kind kind() {
return Kind.REFERENCE;
}
@Override
public boolean canSerializeIn(HolderOwner<T> owner) {
return true;
}
}
private static final class UnsupportedPlacement extends StructurePlacement {
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public StructurePlacementType<?> type() {
return null;
}
}
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
private MutableStructureSetHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingPlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public int[] placeStructure(");
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
String method = source.substring(methodStart, methodEnd);
int catchStart = method.indexOf("catch (RuntimeException e)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -0,0 +1,103 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingServerLevelAdviceContractTest {
@Test
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(transformer.contains("takesArgument(0, MinecraftServer.class)"));
assertTrue(transformer.contains("takesArgument(5, LevelStem.class)"));
assertFalse(transformer.contains("takesArgument(12, ChunkGenerator.class)"));
assertFalse(transformer.contains("takesArgument(13, ChunkGenerator.class)"));
assertTrue(advice.contains("@Advice.AllArguments Object[] constructorArguments"));
assertTrue(advice.contains("argument instanceof ChunkGenerator candidate"));
assertFalse(advice.contains("@Advice.Argument(12)"));
assertFalse(advice.contains("@Advice.Argument(13)"));
}
@Test
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class LevelStorageAccessAdvice",
"private static class ServerLevelAdvice");
assertTrue(transformer.contains(
".type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))"));
assertTrue(transformer.contains("Advice.to(LevelStorageAccessAdvice.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArguments(4)"));
assertTrue(transformer.contains(
"ElementMatchers.takesArgument(0, LevelStorageSource.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(1, String.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(2, Path.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(3, ResourceKey.class)"));
assertTrue(advice.contains("@Advice.Argument(1) String levelId"));
assertTrue(advice.contains(
"@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType"));
assertTrue(advice.contains("getDeclaredMethod(\"peekStemGenerator\", String.class)"));
assertTrue(advice.contains(".invoke(null, levelId)"));
assertTrue(advice.contains(
"dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);"));
assertFalse(advice.contains("dimensionRoot("));
assertFalse(advice.contains("Files.copy"));
}
@Test
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(advice.contains(
"@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey"));
assertTrue(advice.contains("getMethod(\"getTarget\")"));
assertTrue(advice.contains("getMethod(\"getWorld\")"));
assertTrue(advice.contains("getMethod(\"identity\")"));
assertTrue(advice.contains("\"iris\".equals(worldIdentifier.getNamespace())"));
assertTrue(advice.contains("\"minecraft\".equals(worldIdentifier.getNamespace())"));
int resolvedStem = advice.indexOf("Object resolvedStem = stemMethod.invoke(");
int validatedStem = advice.indexOf("resolvedStem instanceof LevelStem runtimeStem");
int identityPublication = advice.indexOf(
"dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);");
int stemPublication = advice.indexOf("levelStem = runtimeStem;");
assertTrue(resolvedStem >= 0);
assertTrue(validatedStem > resolvedStem);
assertTrue(identityPublication > validatedStem);
assertTrue(stemPublication > identityPublication);
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -0,0 +1,28 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class NMSBindingShutdownBoundaryContractTest {
@Test
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
assertTrue(boundary.contains("server.hasFullyShutdown"));
assertTrue(boundary.contains("server.getRunningThread()"));
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingStructureFailureContractTest {
@Test
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
List<String> methodNames = List.of(
"getStructureKeys",
"getStructureSetKeys",
"getReachableStructureKeys",
"getStructureBiomeKeys",
"getPossibleBiomeKeys");
for (String methodName : methodNames) {
String method = methodSource(source, methodName);
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
assertFalse(methodName + " must not log and return an empty registry result",
method.contains("IrisLogging.reportError"));
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
}
}
private static String methodSource(String source, String methodName) {
int start = source.indexOf("public KList<String> " + methodName + "(");
int end = source.indexOf("\n @Override", start + 1);
if (start < 0 || end < 0) {
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
}
return source.substring(start, end);
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class NMSBindingTileDeserializeContractTest {
@Test
public void ownedRegionTileDataMergesBeforeScheduledFallback() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public void deserializeTile(");
int methodEnd = source.indexOf("\n private void merge(", methodStart);
String method = source.substring(methodStart, methodEnd);
int ownershipCheck = method.indexOf("J.isOwnedByCurrentRegion(");
int synchronousMerge = method.indexOf("merge(level, blockPos, tag);", ownershipCheck);
int synchronousReturn = method.indexOf("return;", synchronousMerge);
int scheduledFallback = method.indexOf("J.runAt(pos, () -> merge(level, blockPos, tag))");
assertTrue(ownershipCheck >= 0);
assertTrue(synchronousMerge > ownershipCheck);
assertTrue(synchronousReturn > synchronousMerge);
assertTrue(scheduledFallback > synchronousReturn);
}
}
@@ -0,0 +1,323 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructureWorldgenAccessTest {
private static final int GENERATION_CENTER_X = 60;
private static final int GENERATION_CENTER_Z = 15;
private static final int SURFACE_FIRST_FREE_Y = 80;
private static final int FLOOR_FIRST_FREE_Y = 70;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int x = generationCenter.getMiddleBlockX();
int z = (generationCenter.z() + 2) << 4;
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
assertNull(access.getChunkIfLoadedImmediately(
generationCenter.x(), generationCenter.z() + 2));
List<BlockState> streamed = access.getBlockStates(new AABB(
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
assertEquals(1, streamed.size());
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
generationCenter.getMiddleBlockX(),
FLOOR_FIRST_FREE_Y,
(generationCenter.z() + 2) << 4);
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
access.updateNeighborsAt(position, Blocks.DIRT);
access.neighborShapeChanged(
Direction.NORTH, position, position.north(),
Blocks.DIRT.defaultBlockState(), 2, 512);
access.levelEvent(null, 2001, position, 0);
access.gameEvent(null, Vec3.atCenterOf(position), null);
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(0, recording.mutations);
assertEquals(0, recording.events);
assertEquals(0, recording.blockTicks.count());
}
@Test
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
(generationCenter.x() + 1) << 4,
FLOOR_FIRST_FREE_Y,
(generationCenter.z() - 1) << 4);
assertTrue(access.isInsideGenerationRegion(
generationCenter.x() + 1, generationCenter.z() - 1));
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertEquals(91, access.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
assertTrue(access.ensureCanWrite(position));
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertTrue(access.removeBlock(position, false));
assertTrue(access.destroyBlock(position, false));
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(1, recording.terrainReads);
assertEquals(1, recording.heightReads);
assertEquals(4, recording.mutations);
assertEquals(1, recording.blockTicks.count());
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
assertNull(access.getChunk(generationCenter.getWorldPosition()));
assertEquals(1, recording.statuslessChunkReads);
assertEquals(0, recording.statusChunkReads);
}
@Test
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int safeMaxX = generationCenter.getMaxBlockX() + 17;
int safeMinZ = generationCenter.getMinBlockZ() - 16;
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
AABB disjoint = new AABB(
safeMaxX, -64, safeMinZ,
safeMaxX + 16, 320, safeMaxZ);
AABB overlap = new AABB(
safeMaxX - 8, -128, safeMinZ - 8,
safeMaxX + 16, 400, safeMaxZ + 8);
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
access.getEntities((Entity) null, overlap, entity -> true);
access.getEntities(type, overlap, entity -> true);
assertEquals(2, recording.entityAreas.size());
for (AABB delegatedArea : recording.entityAreas) {
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
assertEquals(-64, delegatedArea.minY, 0D);
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
assertEquals(320, delegatedArea.maxY, 0D);
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
}
}
private static NativeStructureWorldgenAccess access(RecordingDelegate recording) {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y);
}
private static ChunkPos generationCenter() {
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
}
private static final class RecordingDelegate implements InvocationHandler {
private final Holder<Biome> biome;
private final BiomeManager biomeManager;
private final RecordingTicks<Block> blockTicks;
private final RecordingTicks<Fluid> fluidTicks;
private final List<AABB> entityAreas;
private int terrainReads;
private int heightReads;
private int chunkReads;
private int statuslessChunkReads;
private int statusChunkReads;
private int mutations;
private int events;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
this.blockTicks = new RecordingTicks<>();
this.fluidTicks = new RecordingTicks<>();
this.entityAreas = new ArrayList<>();
}
private WorldGenLevel world() {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) {
String name = method.getName();
if (name.equals("getSeaLevel")) {
return 63;
}
if (name.equals("getBiome")) {
return biome;
}
if (name.equals("getBiomeManager")) {
return biomeManager;
}
if (name.equals("getBlockTicks")) {
return blockTicks;
}
if (name.equals("getFluidTicks")) {
return fluidTicks;
}
if (name.equals("getMinY")) {
return -64;
}
if (name.equals("getHeight") && arguments == null) {
return 384;
}
if (name.equals("getHeight")) {
heightReads++;
return 91;
}
if (name.equals("getBlockState")) {
terrainReads++;
return Blocks.DIRT.defaultBlockState();
}
if (name.equals("getFluidState")) {
terrainReads++;
return Blocks.WATER.defaultBlockState().getFluidState();
}
if (name.equals("getChunk")) {
chunkReads++;
if (arguments.length == 2) {
statuslessChunkReads++;
} else {
statusChunkReads++;
}
return null;
}
if (name.equals("getChunkIfLoadedImmediately")) {
chunkReads++;
return null;
}
if (name.equals("getEntities")) {
entityAreas.add((AABB) arguments[1]);
return List.of();
}
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
mutations++;
return true;
}
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
mutations++;
return null;
}
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
events++;
return null;
}
if (name.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (name.equals("equals")) {
return proxy == arguments[0];
}
if (name.equals("toString")) {
return "native-structure-worldgen-test-delegate";
}
throw new UnsupportedOperationException(method.toString());
}
}
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
private int count;
@Override
public void schedule(ScheduledTick<T> tick) {
count++;
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return false;
}
@Override
public int count() {
return count;
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return false;
}
}
}
@@ -0,0 +1,38 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* A registered structure with an EMPTY biome filter is legal datapack content: opt-in structures
* (e.g. Towns and Towers' towns_and_towers:exclusives/*) ship biome tags that only reference
* optional modded biomes, so the filter resolves to zero entries on a vanilla server. Resolving
* biome keys for such a structure must return an empty set (the structure is simply unreachable),
* never throw — a throw here aborts the whole /iris structure verify run and degrades /iris find.
* The defensive throw is only kept for a NON-empty filter whose holders all fail to resolve keys.
*/
public class VanillaStructureBiomesEmptyFilterContractTest {
@Test
public void emptyBiomeFilterReturnsEmptyKeysInsteadOfThrowing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.vanillaStructureBiomesSource")));
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
assertFalse(source.contains("exposes no registered biome keys"));
}
@Test
public void moddedHookMatchesEmptyFilterContract() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.moddedStructureHooksSource")));
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
assertFalse(source.contains("exposes no registered biome keys"));
}
}
@@ -0,0 +1,328 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class WorldgenTerrainHeightmapsTest {
private static final int MIN_Y = -64;
private static final int HEIGHT = 384;
private static final int LAND_TERRAIN_TOP = 70;
private static final int LAND_CANOPY_TOP = 80;
private static final int OCEAN_FLOOR_TOP = 60;
private static final int OCEAN_FLUID_TOP = 64;
private static final int LAND_X = 3;
private static final int LAND_Z = 5;
private static final int OCEAN_X = 8;
private static final int OCEAN_Z = 8;
private static final int TERRAIN_SLAB_DEPTH = 4;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
}
@Test
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
WorldGenLevel world = world(chunks);
StructureStart start = startInOrigin();
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
assertEquals(LAND_CANOPY_TOP, canopySnap);
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
}
@Test
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
for (int chunkX = -1; chunkX <= 1; chunkX++) {
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
}
}
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
for (ChunkAccess chunk : chunks.values()) {
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
}
@Test
public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0));
ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
chunks.put(ChunkPos.pack(1, 0), neighbour);
chunks.put(ChunkPos.pack(2, 0), distanceTwo);
WorldGenLevel world = world(chunks);
StructureStart shifted = startInOrigin();
shifted.getPieces().forEach(piece -> piece.move(16, 0, 0));
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(shifted),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
}
@Test
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
private static int iglooSnapOffset(WorldGenLevel world) {
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
- IglooPieces.GENERATION_HEIGHT - 1;
}
private static StructureStart startInOrigin() {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static IntBinaryOperator surfaceFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static IntBinaryOperator floorFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static boolean isOcean(int x, int z) {
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
}
private static ProtoChunk terrainChunk(ChunkPos pos) {
ProtoChunk chunk = new ProtoChunk(
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
containerFactory(), null);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
}
if (ocean) {
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
}
continue;
}
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
}
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
}
}
}
return chunk;
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("hasChunk")) {
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
}
if (methodName.equals("getChunk") && arguments.length == 2) {
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
if (chunk == null) {
throw new AssertionError("Heightmap priming requested an unavailable chunk "
+ arguments[0] + "," + arguments[1]);
}
return chunk;
}
if (methodName.equals("getHeight") && arguments.length == 3) {
Heightmap.Types type = (Heightmap.Types) arguments[0];
int x = (int) arguments[1];
int z = (int) arguments[2];
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
if (chunk == null) {
throw new AssertionError("Heightmap read requested an unavailable chunk at "
+ x + "," + z);
}
return chunk.getHeight(type, x & 15, z & 15) + 1;
}
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
if (chunk == null) {
throw new AssertionError("Block read requested an unavailable chunk at " + position);
}
return chunk.getBlockState(position);
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "worldgen-heightmap-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
}
@@ -0,0 +1,294 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class VillageImporterListPoolElementRuntimeContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception {
StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown")
.apply(StructureTemplatePool.Projection.RIGID);
ListPoolElement towers = new ListPoolElement(
List.of(tower, overgrown),
StructureTemplatePool.Projection.RIGID
);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers);
Map<String, Object> entry = VillageImporter.piecePoolEntry(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
1
);
StructureLoss loss = VillageImporter.listElementFallbackLoss(
resolution,
"minecraft:pillager_outpost/towers"
);
Map<String, Object> basePlatePiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate",
List.of(Map.of("name", "minecraft:bottom")),
0
);
Map<String, Object> watchtowerPiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
List.of(),
1155
);
VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember(
"minecraft:pillager_outpost/features",
"qa/pillager_outpost/pool/minecraft/pillager_outpost/features",
false,
2,
"minecraft:empty",
"minecraft:pillager_outpost/feature_plate",
"qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate",
1,
0,
List.of(Map.of("name", "minecraft:bottom")));
assertSame(tower, resolution.physicalElement());
assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation());
assertEquals(1, resolution.omittedElements());
assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece"));
assertEquals(1, entry.get("weight"));
assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability());
assertEquals("list_pool_overlays_not_imported", loss.code());
assertEquals(false, basePlatePiece.get("collidable"));
assertFalse(watchtowerPiece.containsKey("collidable"));
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition());
assertTrue(connectorScaffold.losses().isEmpty());
}
@Test
public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception {
StructurePoolElement waystone = StructurePoolElement
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"dungeons_and_taverns:waystones",
"qa/pool/dungeons_and_taverns/waystones",
false,
1,
"dungeons_and_taverns:waystones",
resolution.templateLocation(),
"qa/piece/dungeons_and_taverns/waystones/waystone_scaffold",
2,
0,
List.of());
assertSame(waystone, resolution.physicalElement());
assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition());
assertEquals(true, normalization.poolEntry().get("empty"));
assertEquals(2, normalization.poolEntry().get("weight"));
assertEquals("connectorless_all_air_member_normalized_empty",
normalization.losses().getFirst().code());
}
@Test
public void actualNonAirOrphanMemberIsOmitted() throws Exception {
StructurePoolElement orphan = StructurePoolElement
.legacy("test:orphan_house")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:orphan_pool",
"qa/pool/test/orphan_pool",
false,
1,
null,
resolution.templateLocation(),
"qa/piece/test/orphan_house",
4,
37,
List.of());
assertSame(orphan, resolution.physicalElement());
assertEquals("test:orphan_house", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_non_air_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)"));
assertTrue(normalization.losses().getFirst().detail().contains("no source fallback"));
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualConnectorlessStartMemberRemainsPhysical() throws Exception {
StructurePoolElement start = StructurePoolElement
.legacy("test:air_start")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:start",
"qa/pool/test/start",
true,
2,
"test:fallback",
resolution.templateLocation(),
"qa/piece/test/air_start",
6,
0,
List.of());
assertSame(start, resolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition());
assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece"));
assertTrue(normalization.losses().isEmpty());
}
@Test
public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception {
StructurePoolElement inert = StructurePoolElement
.legacy("test:mixed_air_inert")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:mixed",
resolution.templateLocation(),
"qa/piece/test/mixed_air_inert",
9,
0,
List.of());
assertSame(inert, resolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_all_air_mixed_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:mixed_all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:mixed_non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/mixed_all_air_primary",
9,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/mixed_non_air_primary",
12,
41,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece"));
assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
@Test
public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/all_air_primary",
5,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/non_air_primary",
7,
29,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece"));
assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
}
@@ -0,0 +1,508 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisJigsawConfiguration;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
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.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFactoryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void genericSourceDoesNotRequireJigsawType() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
Structure configured = NativeStructureFactory.configure(
null, source, null, false, 80);
assertSame(source, configured);
}
@Test
public void jigsawOptionsFailClosedForNonJigsawSource() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
null, source, new IrisJigsawConfiguration(), false, 80));
}
@Test
public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() {
CompoundTag scalar = new CompoundTag();
scalar.putInt("max_distance_from_center", 80);
CompoundTag axisSpecific = new CompoundTag();
CompoundTag distance = new CompoundTag();
distance.putInt("horizontal", 96);
distance.putInt("vertical", 64);
axisSpecific.put("max_distance_from_center", distance);
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal"));
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical"));
assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal"));
assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical"));
}
@Test
public void sourceMetadataIncludesAdjustBoundingBoxExpansion() {
Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty())
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
.build());
assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none));
assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox));
}
@Test
public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() {
StructurePoolElement narrow = inlineElement(30, 8, 70);
StructurePoolElement wide = inlineElement(96, 8, 12);
ListPoolElement list = new ListPoolElement(
List.of(narrow, wide, EmptyPoolElement.INSTANCE),
StructureTemplatePool.Projection.RIGID);
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(inlineElement(110, 8, 20), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1)));
startHolder.bind(start);
assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
}
@Test
public void oversizedInlineStartTemplateReportsItsFullSpan() {
BoundPoolHolder holder = new BoundPoolHolder("test:oversized");
StructureTemplatePool pool = new StructureTemplatePool(
holder,
List.of(Pair.of(inlineElement(160, 8, 9), 1)));
holder.bind(pool);
assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() {
BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty");
StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of());
emptyHolder.bind(empty);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1)));
startHolder.bind(start);
BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement");
StructureTemplatePool replacement = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1)));
replacementHolder.bind(replacement);
BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated");
StructureTemplatePool unrelated = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1)));
unrelatedHolder.bind(unrelated);
ResourceKey<StructureTemplatePool> startKey = startHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> replacementKey = replacementHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> childAlias = ResourceKey.create(
Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias"));
List<PoolAliasBinding> aliases = List.of(
PoolAliasBinding.direct(startKey, replacementKey),
PoolAliasBinding.direct(childAlias, unrelatedKey));
Map<ResourceKey<StructureTemplatePool>, StructureTemplatePool> pools = Map.of(
replacementKey, replacement,
unrelatedKey, unrelated);
assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startKey, aliases, pools::get));
}
@Test
public void sourceStartSpanDoesNotIncludeItsFallbackPool() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1)));
startHolder.bind(start);
assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null));
}
@Test
public void customPoolElementUsesItsBoundingBoxCapability() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of());
fallbackHolder.bind(fallback);
BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom");
StructureTemplatePool pool = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1)));
poolHolder.bind(pool);
assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() {
ChunkGenerator delegate = new TestChunkGenerator();
Holder<Biome> biome = Holder.direct((Biome) null);
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
delegate, biome);
assertEquals(63, generator.getSeaLevel());
assertEquals(64, generator.getBaseHeight(
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
NoiseColumn column = generator.getBaseColumn(
0, 0, heightAccessor, null);
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
assertSame(Blocks.STONE, column.getBlock(81).getBlock());
assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource());
}
@Test
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
StructureStart start = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(piece))
);
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
start, source, -20, LevelHeightAccessor.create(-64, 384));
assertNotSame(start, relocated);
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
assertEquals(-20, relocated.getBoundingBox().minY());
}
@Test
public void invalidGeneratedStartIsANormalSkippedCandidate() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
StructureStart valid = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(
new SwampHutPiece(RandomSource.create(23L), 0, 0)))
);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
StructureStart.INVALID_START));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
new StructureStart(source, new ChunkPos(0, 0), 0,
new PiecesContainer(List.of()))));
assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid));
}
@Test
public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0);
boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0);
StructureStart boundary = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(boundaryPiece))
);
SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0);
oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0);
StructureStart oversized = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(oversizedPiece))
);
StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication(
boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content");
StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication(
oversized, source, 0,
new IrisStructureTerrain(),
"test:oversized_content");
assertTrue(boundaryResult.isValid());
assertSame(StructureStart.INVALID_START, result);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result));
assertEquals(1, oversized.getPieces().size());
}
@Test
public void failedManualGenerationClearsStaleOwnershipAfterReplacementHandling() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
StandardCharsets.UTF_8);
int failureStart = source.indexOf("if (!isUsableGeneratedStart(generated))");
int failureEnd = source.indexOf("continue;", failureStart);
String failureBranch = source.substring(failureStart, failureEnd);
assertTrue(failureBranch.contains("if (replacement)"));
assertTrue(failureBranch.indexOf("NativeStructureOwnershipStore.discard(")
> failureBranch.lastIndexOf("}") );
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipFingerprint.capture("));
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
}
@Test
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
StandardCharsets.UTF_8);
int ownershipRecord = source.indexOf("NativeStructureOwnershipStore.record(");
int publication = source.indexOf("context.structureManager().setStartForStructure(", ownershipRecord);
int cleanup = source.indexOf("NativeStructureOwnershipStore.discard(", publication);
int suppressed = source.indexOf("publicationError.addSuppressed(cleanupError)", cleanup);
int rethrow = source.indexOf("throw publicationError;", suppressed);
assertTrue(ownershipRecord >= 0);
assertTrue(publication > ownershipRecord);
assertTrue(cleanup > publication);
assertTrue(suppressed > cleanup);
assertTrue(rethrow > suppressed);
}
private static final class TestChunkGenerator extends ChunkGenerator {
private TestChunkGenerator() {
super(new FixedBiomeSource(Holder.direct((Biome) null)));
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(this);
}
@Override
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState,
BiomeManager biomeManager, StructureManager structureManager,
ChunkAccess chunk) {
}
@Override
public void buildSurface(WorldGenRegion level, StructureManager structureManager,
RandomState randomState, ChunkAccess protoChunk) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(
Blender blender, RandomState randomState,
StructureManager structureManager, ChunkAccess centerChunk) {
return CompletableFuture.completedFuture(centerChunk);
}
@Override
public int getSeaLevel() {
return 63;
}
@Override
public int getMinY() {
return -64;
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor heightAccessor, RandomState randomState) {
return 64;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
RandomState randomState) {
BlockState[] states = new BlockState[heightAccessor.getHeight()];
for (int index = 0; index < states.length; index++) {
states[index] = Blocks.STONE.defaultBlockState();
}
return new NoiseColumn(heightAccessor.getMinY(), states);
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState randomState,
BlockPos feetPos) {
}
}
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
StructureTemplate template = new StructureTemplate();
CompoundTag tag = new CompoundTag();
ListTag size = new ListTag();
size.add(IntTag.valueOf(sizeX));
size.add(IntTag.valueOf(sizeY));
size.add(IntTag.valueOf(sizeZ));
tag.put("size", size);
template.load(BuiltInRegistries.BLOCK, tag);
return new InlinePoolElement(template);
}
private static final class InlinePoolElement extends SinglePoolElement {
private final StructureTemplate inlineTemplate;
private InlinePoolElement(StructureTemplate template) {
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
inlineTemplate = template;
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return inlineTemplate.getBoundingBox(
new StructurePlaceSettings().setRotation(rotation), position);
}
}
private static final class BoundingPoolElement extends StructurePoolElement {
private final int width;
private final int depth;
private BoundingPoolElement(int width, int depth) {
super(StructureTemplatePool.Projection.RIGID);
this.width = width;
this.depth = depth;
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(width, 1, depth);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(
position.getX(), position.getY(), position.getZ(),
position.getX() + width - 1, position.getY(),
position.getZ() + depth - 1);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world,
StructureManager structureManager, ChunkGenerator chunkGenerator,
BlockPos position, BlockPos pivot, Rotation rotation,
BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
}
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
private BoundPoolHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
}
private void bind(StructureTemplatePool pool) {
bindValue(pool);
}
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.nativegen;
import net.minecraft.SharedConstants;
import net.minecraft.core.Vec3i;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureGenerationKeysContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void reachabilityUsesEnabledActiveStructureSetsWithoutLazyPlacementGeneration() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.nativeStructureGenerationKeysSource")));
assertTrue(source.contains("getWorldGenSettings().options().generateStructures()"));
assertTrue(source.contains("getGeneratorState().possibleStructureSets()"));
assertTrue(source.contains("isEnabledPlacement(structureSet.placement())"));
assertTrue(source.contains("StructurePlacement.class.getDeclaredMethods()"));
assertTrue(source.contains("entry.weight() <= 0"));
assertTrue(source.contains("hasPossibleBiome(entry.structure().value(), possibleBiomes)"));
assertFalse(source.contains("getPlacementsForStructure("));
}
@Test
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
}
private static RandomSpreadStructurePlacement placement(float frequency) {
return new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
frequency,
1,
Optional.empty(),
2,
1,
RandomSpreadType.LINEAR);
}
}
@@ -0,0 +1,197 @@
package art.arcane.iris.nativegen;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureLocatePersistenceTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void storageProbeScansEachCandidateOnlyOnce() {
AtomicInteger reads = new AtomicInteger();
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
chunk.putString("Status", "full");
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 0);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
reads.incrementAndGet();
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
ChunkPos chunkPos = new ChunkPos(7, -11);
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertEquals(1, reads.get());
assertEquals(1, budget.used());
}
@Test
public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() {
AtomicBoolean datafixed = new AtomicBoolean();
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
legacy.putString("Status", "full");
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
datafixed.set(true);
CompoundTag fixed = storedChunk.copy();
fixed.putInt("DataVersion",
SharedConstants.getCurrentVersion().dataVersion().version());
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
fixed.put("structures", structures);
return fixed;
});
assertFalse(budget.acceptsStored(
null, new ChunkPos(3, 4), "minecraft:village_plains", true));
assertTrue(datafixed.get());
}
@Test
public void partialDatafixFailureFallsBackToSelectedChunkVerification() {
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
throw new IllegalStateException("broken partial datafix");
});
assertTrue(budget.acceptsStored(
null, new ChunkPos(9, -2), "minecraft:village_plains", true));
}
@Test
public void missingStartsContainerFallsBackToSelectedChunkVerification() {
CompoundTag chunk = currentChunk();
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", false));
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", true));
}
@Test
public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag structures = new CompoundTag();
structures.put("starts", new CompoundTag());
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertFalse(budget.acceptsStored(
null, new ChunkPos(-3, 2), "minecraft:village_plains", false));
}
@Test
public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag tombstone = new CompoundTag();
tombstone.putString("id", "INVALID");
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", tombstone);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(12, -9);
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
@Test
public void malformedNonCompoundStartIsRejectedWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag starts = new CompoundTag();
starts.putInt("minecraft:village_plains", 1);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
assertFalse(budget(chunk).acceptsStored(
null, new ChunkPos(1, 1), "minecraft:village_plains", false));
}
@Test
public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(-4, -7);
assertTrue(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
private static CompoundTag currentChunk() {
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
return chunk;
}
private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) {
return new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
}
}
@@ -0,0 +1,346 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.ListTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipFingerprintTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() {
long seed = 8372619L;
ChunkPos origin = new ChunkPos(9, -4);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
String beforeReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", wrapped);
ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null);
PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null);
assertEquals(1, loadedPieces.pieces().size());
assertTrue(loadedPieces.pieces().getFirst()
instanceof OceanMonumentPieces.MonumentBuilding);
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, loadedPieces);
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", reloaded);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
assertEquals(beforeReload, afterReload);
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded));
assertEquals(alignedLocatorY, ownership.locatorY());
assertEquals(1, reloaded.getPieces().size());
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void sameKeyAndOriginStillRequiresMatchingContentIdentity() {
long seed = 18273L;
ChunkPos origin = new ChunkPos(2, 3);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
expected,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()),
expected.getBoundingBox());
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
}
@Test
public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() {
long seed = 772931L;
ChunkPos origin = new ChunkPos(4, -7);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(wrapped.getPieces()));
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
ChunkPos target = outsideLiveBounds(ownership, reloaded);
assertNotNull(target);
assertTrue(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, ownership));
assertFalse(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, null));
}
@Test
public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos westOrigin = new ChunkPos(0, 0);
ChunkPos eastOrigin = new ChunkPos(16, 0);
StructureStart west = monumentStart(structure, westOrigin, 317L);
StructureStart east = monumentStart(structure, eastOrigin, 941L);
west.getPieces().getFirst().move(112, 0, 0);
east.getPieces().getFirst().move(-96, 0, 0);
NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure);
NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure);
ChunkPos shared = new ChunkPos(8, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects(
NativeStructureReferenceEnvelope.contentBounds(east)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", west, westOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", east, eastOwnership));
}
@Test
public void denseAdjacentManualStartsDoNotSuppressEachOther() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos firstOrigin = new ChunkPos(0, 0);
ChunkPos secondOrigin = new ChunkPos(1, 0);
StructureStart first = monumentStart(structure, firstOrigin, 17L);
StructureStart second = monumentStart(structure, secondOrigin, 23L);
NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure);
NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure);
ChunkPos shared = new ChunkPos(0, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects(
NativeStructureReferenceEnvelope.contentBounds(second)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", first, firstOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", second, secondOwnership));
}
@Test
public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement());
StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement());
assertEquals(
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first),
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded));
}
@Test
public void namedPoolTemplateRemainsPartOfTheStableFingerprint() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructurePoolElement firstElement = StructurePoolElement.single("test:first")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement secondElement = StructurePoolElement.single("test:second")
.apply(StructureTemplatePool.Projection.RIGID);
assertNotEquals(
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, firstElement)),
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, secondElement)));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure,
origin,
0,
new PiecesContainer(List.of(building))
);
}
private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId("monument-test")
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
baseY
);
}
private static NativeStructureOwnershipRecord ownership(
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
return NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
start,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()),
NativeStructureReferenceEnvelope.referenceBounds(
start, structure, new IrisStructureTerrain())
);
}
private static StructureStart poolStart(
OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) {
BlockPos position = new BlockPos(
origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ());
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, element, position, 0, Rotation.NONE,
new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(piece)));
}
private static final class IdentityStringPoolElement extends StructurePoolElement {
private IdentityStringPoolElement() {
super(StructureTemplatePool.Projection.RIGID);
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(1, 1, 1);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(position);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world, StructureManager structureManager,
ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot,
Rotation rotation, BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
@Override
public String toString() {
return "IdentityStringPoolElement@"
+ Integer.toHexString(System.identityHashCode(this));
}
}
private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership,
StructureStart start) {
for (int chunkX = ownership.referenceMinChunkX();
chunkX <= ownership.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = ownership.referenceMinChunkZ();
chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) {
ChunkPos candidate = new ChunkPos(chunkX, chunkZ);
if (!start.getBoundingBox().intersects(
candidate.getMinBlockX(), candidate.getMinBlockZ(),
candidate.getMaxBlockX(), candidate.getMaxBlockZ())) {
return candidate;
}
}
}
return null;
}
}
@@ -0,0 +1,314 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.StructurePlacementGrid;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipRecoveryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() {
long seed = 5519231L;
ChunkPos origin = new ChunkPos(7, -5);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
expected, 0, 80, -64, 320);
int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected);
NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24);
StructureStart reloaded = new StructureStart(
structure,
origin,
expected.getReferences(),
OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(expected.getPieces()))
);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, reloaded,
plan, expected, false);
assertNotNull(recovered);
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
recovered.placementIdentity());
assertEquals(expectedLocatorY, recovered.locatorY());
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
recovered.locatorY());
assertEquals(IrisStructureTerrainMode.FORCE_CARVE,
recovered.restoredDecision().terrain().resolvedMode());
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
assertTrue(recovered.referenceMinChunkX()
< NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4);
}
@Test
public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() {
long seed = 22817L;
ChunkPos origin = new ChunkPos(-3, 6);
OceanMonumentStructure structure = structure();
StructureStart natural = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, natural,
plan, natural, true);
assertNull(recovered);
}
@Test
public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() {
long seed = 991723L;
ChunkPos origin = new ChunkPos(2, 4);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart changedGeometry = monumentStart(structure, origin, seed);
for (StructurePiece piece : changedGeometry.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureStartPlan originalIdentity = plan(
origin, "original-identity", 0);
NativeStructureStartPlan changedIdentity = plan(
new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0);
assertNotEquals(
StructurePlacementGrid.placementIdentity(originalIdentity.placement()),
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
changedIdentity, expected, false));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, changedGeometry,
plan(origin, "geometry-check", 0), expected, false));
}
@Test
public void recoveredEnvelopeStillRequiresExactFingerprint() {
long seed = 81337L;
ChunkPos origin = new ChunkPos(-8, -9);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
plan(origin, "fingerprint-check", 16), expected, false);
assertNotNull(recovered);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(0, 0, 1);
}
assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved));
}
@Test
public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() {
String structureKey = "minecraft:monument";
long seed = 648231L;
ChunkPos origin = new ChunkPos(5, -6);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, content);
NativeStructureOwnershipRecord refreshed =
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, stale);
assertNotNull(refreshed);
assertNotEquals(stale, refreshed);
assertEquals(stale.schema(), refreshed.schema());
assertEquals(stale.ownershipKey(), refreshed.ownershipKey());
assertEquals(stale.placementIdentity(), refreshed.placementIdentity());
assertEquals(stale.baseY(), refreshed.baseY());
assertEquals(stale.locatorY(), refreshed.locatorY());
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
assertEquals(stale.decision(), refreshed.decision());
assertEquals(IrisStructureTerrainMode.VACUUM,
refreshed.restoredDecision().terrain().resolvedMode());
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX());
assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX());
assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ());
assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ());
assertTrue(hasExpandedCoverage(stale, refreshed));
assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, refreshed));
}
@Test
public void staleEnvelopeCannotRefreshAgainstDifferentContent() {
String structureKey = "minecraft:monument";
long seed = 412987L;
ChunkPos origin = new ChunkPos(-2, 7);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, expected, plan,
NativeStructureReferenceEnvelope.contentBounds(expected));
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, moved, stale));
}
@Test
public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() {
String structureKey = "minecraft:monument";
long seed = 927451L;
ChunkPos origin = new ChunkPos(3, 8);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
@Test
public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() {
String structureKey = "minecraft:monument";
long seed = 381729L;
ChunkPos origin = new ChunkPos(0, 0);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
int maximumReferenceBlockX = ((origin.x()
+ NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
int shiftX = maximumReferenceBlockX - initial.maxX();
for (StructurePiece piece : start.getPieces()) {
piece.move(shiftX, 0, 0);
}
NativeStructureStartPlan plan = plan(
origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS,
ownership.referenceMaxChunkX());
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
private static OceanMonumentStructure structure() {
return new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building =
new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(building)));
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
int horizontalPadding) {
return plan(origin, placementId,
IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding);
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
IrisStructureTerrainMode terrainMode,
int horizontalPadding) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(terrainMode)
.setHorizontalPadding(horizontalPadding);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId(placementId)
.setTerrain(terrain)
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
NativeStructureReferenceEnvelope.contentBounds(
monumentStart(structure(), origin, 1L)).minY()
);
}
private static boolean hasExpandedCoverage(
NativeStructureOwnershipRecord stale,
NativeStructureOwnershipRecord refreshed) {
for (int chunkX = refreshed.referenceMinChunkX();
chunkX <= refreshed.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = refreshed.referenceMinChunkZ();
chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) {
if (!stale.covers(chunkX, chunkZ)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,547 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.engine.object.IrisStructureYBand;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorEncaseTest {
private static final long TEST_SEED = 1234L;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(1)
.setCeilingPadding(1)
.setFloorPadding(1),
null);
assertEquals(Blocks.DIRT.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
}
@Test
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
StructureStart start = start(TerrainAdjustment.BURY, -1);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
null);
assertEquals(-1, bounds.minY());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
state(blocks, bounds.minX(), -1, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), 0, bounds.minZ()));
}
@Test
public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() {
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64));
assertEquals(Blocks.STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64));
}
@Test
public void sourceEncaseMaterialFollowsNearbyIrisTerrain() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, 64, 8);
put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, -20, 8);
put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState());
put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
NativeStructureTerrainIntegrator.captureSourceTerrain(
world(new HashMap<>()), area,
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
"test:bounded-snapshot", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
assertNotNull(sourceTerrain);
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
}
@Test
public void customDimensionKeyUsesRegisteredVanillaDimensionType() {
ResourceKey<Level> customDimension = ResourceKey.create(
Registries.DIMENSION, Identifier.parse("iris:custom_nether"));
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.NETHER, null, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.END, null, 64));
}
@Test
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setEncasePalette(palette),
NativeStructurePostProcessorEncaseTest::tuffBlock);
assertEquals(Blocks.TUFF.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
}
@Test
public void encaseFillRunsBeforeNativePlacement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", prepareTerrain);
int placement = source.indexOf("placeVanillaStructure(world, structureManager, random", placementLoop);
assertTrue(prepareTerrain >= 0);
assertTrue(placementLoop > prepareTerrain);
assertTrue(placement > placementLoop);
}
@Test
public void omittedTerrainConfigurationRetainsSourceSemantics() {
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode());
}
}
@Test
public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
int groundY = bounds.minY();
BoundingBox area = new BoundingBox(
bounds.minX() - 7, groundY - 13, bounds.minZ(),
bounds.maxX(), groundY + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:bury", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 6, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 7, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 13, bounds.minZ()));
}
@Test
public void sourceEncapsulateWrapsTheFullPieceBounds() {
StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 13, bounds.minY(), bounds.minZ(),
bounds.maxX(), bounds.maxY() + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:encapsulate", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ()));
}
@Test
public void sourceTerrainDensityOnlyUsesRigidPoolPieces() {
PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING);
PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID);
assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching));
assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid));
}
@Test
public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() {
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0));
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0));
}
@Test
public void explicitTerrainConfigurationWinsOverAutoEncase() {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
}
@Test
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
IrisStructureTerrain source = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.SOURCE);
IrisStructureTerrain preserve = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
IrisStructureTerrain encase = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE);
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BEARD_BOX, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), encase));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), source));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), preserve));
}
@Test
public void encaseReservesTheNeighborChunkEnvelope() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox sourceBounds = generated.getBoundingBox();
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(4);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
generated.getStructure(),
0,
terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(sourceBounds, wrapped.getBoundingBox());
assertEquals(content.minX() - 4, referenceBounds.minX());
assertEquals(content.maxX() + 4, referenceBounds.maxX());
assertEquals(content.minZ() - 4, referenceBounds.minZ());
assertEquals(content.maxZ() + 4, referenceBounds.maxZ());
assertEquals(generated.getPieces().size(), wrapped.getPieces().size());
}
@Test
public void referenceEnvelopeRejectsChunkCoordinateOverflow() {
StructureStart generated = start(TerrainAdjustment.NONE, 64);
StructureStart extreme = new StructureStart(
generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0,
new PiecesContainer(generated.getPieces()));
assertThrows(IllegalStateException.class, () ->
NativeStructureReferenceEnvelope.referenceBounds(
extreme, extreme.getStructure(), new IrisStructureTerrain()));
}
@Test
public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(112);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated, generated.getStructure(), 0, terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(content.minX() - 112, referenceBounds.minX());
assertEquals(content.maxX() + 112, referenceBounds.maxX());
}
@Test
public void yBandRelocatesTheStartMidpointDeterministically() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart first = start(TerrainAdjustment.BURY, 64);
StructureStart second = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
NativeStructureVerticalPlacer.applyVerticalShift(
second, -64, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = first.getBoundingBox();
assertEquals(bounds.minY(), second.getBoundingBox().minY());
assertTrue(bounds.minY() >= -120);
assertTrue(bounds.maxY() <= -20);
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(0, repeated);
}
@Test
public void yBandClampsWhenTheBandCannotContainTheStructure() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
StructureStart start = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = start.getBoundingBox();
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
assertTrue(midpoint >= -50);
assertTrue(midpoint <= -45);
}
@Test
public void yBandReplacesBothTheBlindShiftAndBurial() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getBoundingBox();
int height = bounds.maxY() - bounds.minY();
NativeStructureVerticalPlacer.applyVerticalShift(
start, -200, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
}
@Test
public void preserveSourceYWinsOverTheConfiguredBand() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart start = start(TerrainAdjustment.BURY, 64);
int minY = start.getBoundingBox().minY();
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, true, band, (x, z) -> 40);
assertEquals(0, offset);
assertEquals(minY, start.getBoundingBox().minY());
}
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
assertNotNull(palette);
assertNotNull(rng);
return Blocks.TUFF.defaultBlockState();
}
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) {
BlockPos position = new BlockPos(0, 64, 0);
StructurePoolElement element = StructurePoolElement.single(
"minecraft:empty",
Holder.direct(new StructureProcessorList(List.of(
BlockIgnoreProcessor.STRUCTURE_AND_AIR))),
LiquidSettings.APPLY_WATERLOGGING
).apply(projection);
return new PoolElementStructurePiece(
null,
element,
position,
0,
Rotation.NONE,
new BoundingBox(position),
LiquidSettings.APPLY_WATERLOGGING);
}
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
return state(blocks, position.getX(), position.getY(), position.getZ());
}
if (methodName.equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
BlockState blockState = (BlockState) arguments[1];
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
return true;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "encase-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot(
WorldGenLevel world, BoundingBox area) {
BitSet requiredLayers = new BitSet(area.getYSpan());
requiredLayers.set(0, area.getYSpan());
return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture(
world, area, requiredLayers);
}
private static void put(Map<BlockPos, BlockState> blocks,
int x, int y, int z, BlockState state) {
blocks.put(new BlockPos(x, y, z), state);
}
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
}
}
@@ -0,0 +1,202 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorMonumentTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(39, start.getBoundingBox().minY());
assertEquals(61, start.getBoundingBox().maxY());
}
@Test
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(26, building.getBoundingBox().minY());
assertEquals(48, building.getBoundingBox().maxY());
assertEquals(26, start.getBoundingBox().minY());
assertEquals(48, start.getBoundingBox().maxY());
assertTrue(children.size() > 20);
for (int i = 0; i < children.size(); i++) {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@Test
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-10, offset);
assertEquals(29, start.getBoundingBox().minY());
assertEquals(51, start.getBoundingBox().maxY());
}
@Test
public void referenceEnvelopeDoesNotAddMonumentPieces() {
StructureStart start = monumentStart(1337L);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start,
start.getStructure(),
0,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(4));
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
wrapped, "minecraft:monument", 0, 63, -64, 320,
false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512,
false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
ExecutorService executor = Executors.newFixedThreadPool(8);
CountDownLatch release = new CountDownLatch(1);
List<Future<?>> futures = new ArrayList<>();
try {
for (int task = 0; task < 16; task++) {
futures.add(executor.submit(() -> {
release.await();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
reloaded, "minecraft:monument", 0, 50, -256, 512);
return null;
}));
}
release.countDown();
for (Future<?> future : futures) {
future.get();
}
} finally {
executor.shutdownNow();
}
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
return;
}
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
}
private static StructureStart monumentStart(long seed) {
ChunkPos chunkPos = new ChunkPos(0, 0);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
RandomSource.create(seed), -29, -29, Direction.NORTH);
return new StructureStart(
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
}
private static OceanMonumentStructure monumentStructure() {
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
}
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
}
private static int[] minimumYs(List<StructurePiece> pieces) {
int[] ys = new int[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
ys[i] = pieces.get(i).getBoundingBox().minY();
}
return ys;
}
}
@@ -0,0 +1,198 @@
package art.arcane.iris.nativegen;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorScatteredFeatureTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
StructureStart start = desertStart(piece);
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
assertEquals(1, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(65, piece.getBoundingBox().minY());
assertEquals(65, start.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment(-2));
assertEquals(65, piece.getBoundingBox().minY());
}
@Test
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
StructureStart start = jungleStart(piece);
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
assertEquals(11, offset);
assertEquals(75, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(75, piece.getBoundingBox().minY());
}
@Test
public void repeatedAlignmentIsIdempotent() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
assertEquals(17, initialOffset);
assertEquals(0, repeatedOffset);
assertEquals(81, piece.getBoundingBox().minY());
}
@Test
public void scatteredAlignmentStaysInsideWorldBounds() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
StructureStart start = desertStart(piece);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
assertEquals(241, offset);
assertEquals(305, piece.getBoundingBox().minY());
assertEquals(319, start.getBoundingBox().maxY());
}
@Test
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
StructureStart desertStart = desertStart(desert);
StructureStart jungleStart = jungleStart(jungle);
StructureStart swampStart = swampStart(swamp);
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
assertEquals(0, terrainQueries.get());
assertEquals(64, swamp.getBoundingBox().minY());
}
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
assertEquals(int.class, field.getType());
assertTrue(Modifier.isProtected(field.getModifiers()));
assertFalse(Modifier.isFinal(field.getModifiers()));
assertFalse(Modifier.isStatic(field.getModifiers()));
assertTrue(field.trySetAccessible());
}
@Test
public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() {
StructureStart pyramidOverride = swampStart(
new SwampHutPiece(RandomSource.create(43L), 0, 0));
int pyramidMinY = pyramidOverride.getBoundingBox().minY();
int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
pyramidOverride, "minecraft:desert_pyramid", 5,
63, -64, 320, false, false, null, (x, z) -> 200);
assertEquals(5, pyramidOffset);
assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY());
StructureStart monumentOverride = swampStart(
new SwampHutPiece(RandomSource.create(47L), 0, 0));
int monumentMinY = monumentOverride.getBoundingBox().minY();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
monumentOverride, "minecraft:monument", 0, 40, -64, 320);
assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY());
}
private static StructureStart desertStart(StructurePiece piece) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart jungleStart(StructurePiece piece) {
Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart swampStart(StructurePiece piece) {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart start(Structure structure, StructurePiece piece) {
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static final class TestDesertPyramidPiece extends DesertPyramidPiece {
private TestDesertPyramidPiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment(int offset) {
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset);
}
}
private static final class TestJungleTemplePiece extends JungleTemplePiece {
private TestJungleTemplePiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment() {
return updateAverageGroundHeight((LevelAccessor) null, null, 0);
}
}
}
@@ -0,0 +1,291 @@
package art.arcane.iris.nativegen;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
private static final long TEST_SEED = 1234L;
private static final int MIN_Y = -64;
private static final int MAX_Y = 319;
private static final int GROUND_Y = 64;
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
fillLayer(chunk, y, leaves());
}
write(chunk, 4, bounds.minY() - 1, 4, log());
clearVegetation(chunk, start);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
}
}
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
}
@Test
public void buriedStructuresPreserveTheForestAboveThem() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, -20);
write(chunk, 8, GROUND_Y + 1, 8, log());
write(chunk, 8, GROUND_Y + 2, 8, log());
write(chunk, 8, GROUND_Y + 3, 8, log());
write(chunk, 8, GROUND_Y + 4, 8, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
}
@Test
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
int inside = bounds.minY() + 4;
write(chunk, 0, inside, 0, log());
write(chunk, 0, GROUND_Y + 40, 0, leaves());
write(chunk, 12, inside, 12, log());
write(chunk, 12, GROUND_Y + 40, 12, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
}
@Test
public void clearingStopsAtThePieceEnvelopeBoundaries() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
write(chunk, 4, bounds.maxY() + 1, 4, log());
write(chunk, 4, bounds.maxY(), 4, log());
write(chunk, 4, bounds.minY() - 1, 4, log());
write(chunk, 4, bounds.minY() - 2, 4, log());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
}
@Test
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
ProtoChunk chunk = chunk();
StructureStart low = desertStart(0, 0, GROUND_Y);
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
int between = GROUND_Y + 20;
write(chunk, 4, between, 4, log());
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(low, high));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
}
@Test
public void validStartsReachIntersectionProcessing() {
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
desertStart(0, 0, GROUND_Y + 15)));
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
StructureStart.INVALID_START));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@Test
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
}
private static StructureStart desertStart(int originX, int originZ, int topY) {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(
HolderSet.empty(), Map.of(),
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static ProtoChunk chunk() {
return new ProtoChunk(
new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
}
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(chunk, x, y, z, state);
}
}
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static BlockState log() {
return Blocks.OAK_LOG.defaultBlockState();
}
private static BlockState leaves() {
return Blocks.OAK_LEAVES.defaultBlockState();
}
private static BlockState air() {
return Blocks.AIR.defaultBlockState();
}
private static WorldGenLevel world(ChunkAccess chunk) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
return chunk.getBlockState((BlockPos) arguments[0]);
}
if (methodName.equals("setBlock")) {
return chunk.setBlockState(
((BlockPos) arguments[0]).immutable(),
(BlockState) arguments[1], (int) arguments[2]) != null;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "vegetation-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
}
@@ -0,0 +1,236 @@
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.util.valueproviders.ConstantInt;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureTemplateOccupancyTest {
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
private static final int TEMPLATE_WIDTH = 32;
private static final int TERRAIN_HEIGHT = 64;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
// 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla
// runs processors across the whole piece. Iris mirrors the pinned version's semantics.
assertEquals(expectedClippedColumns(), recorder.heightColumns);
}
@Test
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(List.of(
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder);
assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns);
}
@Test
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception {
Method contract = clipContractMethod();
Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null);
for (StructureProcessor processor : List.of(
BlockIgnoreProcessor.STRUCTURE_BLOCK,
JigsawReplacementProcessor.INSTANCE,
LavaSubmergedBlockProcessor.INSTANCE,
NopProcessor.INSTANCE,
BlackstoneReplaceProcessor.INSTANCE,
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new RuleProcessor(List.of()),
new BlockAgeProcessor(0.5F),
protectedBlockProcessor())) {
assertFalse(processor.getClass().getName(),
(Boolean) contract.invoke(processor));
}
assertTrue((Boolean) contract.invoke(
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))));
}
@Test
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
PoolElementStructurePiece piece = straddlingPiece(List.of());
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
}
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
NativeStructureTemplateOccupancy.resolve(
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
PROCESSING_AREA::isInside, amount -> {
});
}
private static PoolElementStructurePiece straddlingPiece(
List<StructureProcessor> processors) throws Exception {
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
return new PoolElementStructurePiece(
null, new InlineSinglePoolElement(template(), processors),
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
}
private static Set<Integer> columns(int minimumX, int maximumX) {
Set<Integer> columns = new TreeSet<>();
for (int x = minimumX; x <= maximumX; x++) {
columns.add(x);
}
return columns;
}
private static Method clipContractMethod() {
try {
return StructureProcessor.class.getMethod("evaluatesEntirePieceState");
} catch (NoSuchMethodException absent) {
return null;
}
}
private static Set<Integer> expectedClippedColumns() {
return clipContractMethod() != null
? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX())
: columns(0, TEMPLATE_WIDTH - 1);
}
private static StructureProcessor protectedBlockProcessor() throws Exception {
// Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2.
Constructor<?> constructor = ProtectedBlockProcessor.class.getConstructors()[0];
Class<?> parameter = constructor.getParameterTypes()[0];
Object argument = parameter == HolderSet.class
? HolderSet.empty()
: TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air"));
return (StructureProcessor) constructor.newInstance(argument);
}
private static StructureTemplateManager forbiddenTemplateManager() {
throw new AssertionError("Inline templates must not resolve a template manager");
}
private static StructureTemplate template() throws Exception {
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
blocks.add(new StructureTemplate.StructureBlockInfo(
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
}
Constructor<StructureTemplate.Palette> constructor =
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
assertTrue(constructor.trySetAccessible());
StructureTemplate template = new StructureTemplate();
template.palettes.add(constructor.newInstance(blocks));
return template;
}
private static WorldGenLevel world(ColumnRecorder recorder) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getHeight") && arguments.length == 3) {
recorder.heightColumns.add((Integer) arguments[1]);
return TERRAIN_HEIGHT;
}
if (methodName.equals("getBlockState")) {
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
return Blocks.STONE.defaultBlockState();
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "occupancy-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, handler);
}
private static final class ColumnRecorder {
private final Set<Integer> heightColumns = new TreeSet<>();
private final Set<Integer> stateColumns = new TreeSet<>();
}
private static final class InlineSinglePoolElement extends SinglePoolElement {
private InlineSinglePoolElement(
StructureTemplate template, List<StructureProcessor> processors) {
super(Either.right(template),
Holder.direct(new StructureProcessorList(processors)),
StructureTemplatePool.Projection.RIGID,
Optional.<LiquidSettings>empty());
}
}
}
@@ -0,0 +1,160 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class NativeStructureVolumeIndexTest {
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
assertEquals(STRUCTURE_KEY, volume.structure());
assertEquals(bounds.minX(), volume.minX());
assertEquals(bounds.minY(), volume.minY());
assertEquals(bounds.minZ(), volume.minZ());
assertEquals(bounds.maxX(), volume.maxX());
assertEquals(bounds.maxY(), volume.maxY());
assertEquals(bounds.maxZ(), volume.maxZ());
}
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null));
}
@Test
public void coldAndCachedQueriesResolveIdenticalVolumes() {
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
int coldResolutions = resolver.resolutions();
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
assertTrue(coldResolutions > 0);
assertFalse(cold.isEmpty());
assertEquals(cold, cached);
assertEquals(coldResolutions, resolver.resolutions());
}
@Test
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
}
@Test
public void volumesOutsideTheQueryRectAreExcluded() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
KList<NativeStructureVolume> miss = index.resolve(null,
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
assertEquals(1, hit.size());
assertTrue(miss.isEmpty());
}
@Test
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
int reach = NativeStructureVolumeIndex.originReachChunks();
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
}
@Test
public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
assertFalse(source.contains("StructureManager"));
assertFalse(source.contains("getStartForStructure"));
assertFalse(source.contains("getAllStarts"));
assertFalse(source.contains("ChunkAccess"));
assertFalse(source.contains("NativeStructureOwnershipStore"));
assertFalse(source.contains("NativeStructureOwnershipRecovery"));
}
@Test
public void vanillaVolumesAreGatedOnPackPolicy() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
assertTrue(source.contains("NativeStructureGenerationPolicy.resolve("));
assertTrue(source.contains("if (!decision.generate())"));
assertTrue(source.contains("isStructureChunk("));
}
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
}
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final int originChunkX;
private final int originChunkZ;
private CountingResolver(int originChunkX, int originChunkZ) {
this.originChunkX = originChunkX;
this.originChunkZ = originChunkZ;
}
private int resolutions() {
return resolutions.get();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
resolutions.incrementAndGet();
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
}
}
}
+76
View File
@@ -0,0 +1,76 @@
String apiVersion = providers.gradleProperty('apiVersion').get()
def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
.get()
dependencies {
compileOnly(project(':core'))
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
compileOnly(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.paper.api)
// LogFilterSVC (constructed in Iris.enable's explicit service list) implements the
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
compileOnly(libs.log4j.core)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api)
testImplementation(libs.bstats)
testImplementation(project(':core'))
testImplementation(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.placeholderApi)
testImplementation(libs.placeholderApi) {
transitive = false
}
}
tasks.named('processResources').configure {
def pluginProperties = [
name : rootProject.name,
version : rootProject.version,
apiVersion: apiVersion,
main : mainClass,
bootstrapper: bootstrapperClass,
]
inputs.properties(pluginProperties)
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
expand(pluginProperties)
}
}
tasks.named('compileJava', JavaCompile).configure {
// lombok.config decides how equals/hashCode/toString are generated (fields, never getters), so editing it
// changes this module's bytecode. Gradle does not know that on its own and would serve a stale up-to-date
// build until something else in the module changed.
inputs.file(rootProject.file('lombok.config'))
.withPropertyName('lombokConfig')
.withPathSensitivity(PathSensitivity.NONE)
}
tasks.named('jar', Jar).configure {
archiveBaseName.set('iris-bukkit-plugin')
}
tasks.named('test').configure {
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
systemProperty('iris.commandIrisSource', file('src/main/java/art/arcane/iris/core/commands/CommandIris.java').absolutePath)
systemProperty('iris.bukkitChunkGeneratorSource', rootProject.file('core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
systemProperty('iris.jigsawStudioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java').absolutePath)
systemProperty('iris.studioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/StudioSVC.java').absolutePath)
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
systemProperty('iris.startupSource', file('src/main/java/art/arcane/iris/Iris.java').absolutePath)
systemProperty('iris.readmeSource', rootProject.file('README.md').absolutePath)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,196 @@
package art.arcane.iris;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
import art.arcane.iris.core.lifecycle.HuskWorldQuarantine;
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import art.arcane.iris.util.common.misc.SlimJar;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
@SuppressWarnings("UnstableApiUsage")
public final class IrisBootstrap implements PluginBootstrap {
@Override
public void bootstrap(BootstrapContext context) {
WorldReplacementBootstrapMarker.markBootstrapped();
try {
loadRuntimeLibraries(context);
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
reconcilePendingWorldReplacements(context, startupPaths);
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
requireUsableWorldStorage(startupPaths);
ProvisionResult provisioned = provision(context, startupPaths);
Path datapackRoot = provisioned.datapackRoot();
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
} catch (Throwable failure) {
armStartupFailure(context, failure);
}
}
private static void loadRuntimeLibraries(BootstrapContext context) {
SlimJar.loadBootstrap(
context.getDataDirectory().resolve("cache").resolve("libraries"),
new SlimJar.BootstrapLogger() {
@Override
public void info(String message) {
context.getLogger().info(message);
}
@Override
public void error(String message) {
context.getLogger().error(message);
}
@Override
public void debug(String message) {
context.getLogger().debug(message);
}
});
}
private static void reconcilePendingWorldReplacements(
BootstrapContext context,
BukkitStartupPaths startupPaths
) {
try {
WorldReplacementBootstrap.ReconcileResult result = WorldReplacementBootstrap.reconcile(
context.getDataDirectory(),
startupPaths.levelRoot(),
startupPaths.bukkitConfiguration(),
message -> context.getLogger().info(message)
);
if (result.transactions() > 0) {
context.getLogger().info(
"Reconciled {} pending Iris world replacement(s): {} published, {} rolled back, {} retained",
result.transactions(),
result.published(),
result.rolledBack(),
result.retained()
);
}
} catch (IOException failure) {
throw new IllegalStateException(
"Unable to reconcile pending Iris world replacements before registry bootstrap",
failure
);
}
}
/**
* Moves a hot-deleted world's husk out of {@code <levelRoot>/dimensions/iris} before anything enumerates
* it.
* <p>
* Deleting a loaded world's folder and letting the server save the level back leaves a directory holding
* only the server's own {@code data/} skeleton. Left there it stops the boot whichever way it is
* classified: as unusable storage it trips the guard below, and as an empty folder it is excluded from
* the dimension registry, which trips Paper's interactive world-migration gate - a prompt that consumes
* no console input and hangs startup forever. The folder is moved rather than deleted, so it stays
* recoverable; {@link HuskWorldQuarantine} refuses anything that is not provably worthless.
*/
static void quarantineWorthlessHusks(BukkitStartupPaths startupPaths, Consumer<String> warn) {
HuskWorldQuarantine.quarantineWorthlessHusks(startupPaths.levelRoot(), warn);
}
/**
* Stops startup when a world the server is about to load carries Iris storage Iris cannot generate from.
* <p>
* The server enumerates levels from {@code <levelRoot>/dimensions/<namespace>/<key>} on disk, so a world
* whose folder is present is going to be loaded whatever Iris does; and CraftBukkit swallows whatever
* {@code getDefaultWorldGenerator} throws and falls back to the vanilla generator, which writes vanilla
* terrain straight into that world's region files. A pack snapshot that has gone missing therefore has
* to be found here, before any level is created, exactly as a corrupt {@code dimensions/<id>.json} is.
* <p>
* A world with no folder at all is not a startup failure: nothing enumerates it, nothing loads it, and
* its bukkit.yml and Multiverse entries are what make restoring the folder a complete recovery. Neither
* is a folder that holds no pack snapshot and no world data - {@link #quarantineWorthlessHusks} has
* already moved those out of the dimensions tree, and one that survives it (a Spigot-layout world folder,
* or a move that failed) owns nothing a vanilla generator could destroy. Both are only reported.
*/
static void requireUsableWorldStorage(BukkitStartupPaths startupPaths) throws IOException {
Path levelRoot = startupPaths.levelRoot();
Path levelName = levelRoot.getFileName();
if (levelName == null || levelName.toString().isBlank()) {
throw new IOException("Configured level root has no startup level id: " + levelRoot);
}
List<IrisWorldStorageEntry> entries = BukkitWorldConfiguration.auditIrisWorldStorage(
startupPaths.bukkitConfiguration().toFile(),
levelName.toString(),
levelRoot
);
StringBuilder unusable = new StringBuilder();
for (IrisWorldStorageEntry entry : entries) {
switch (entry.state()) {
case PRESENT -> {
}
case MISSING -> MissingWorldStorageLog.warnOnce(
entry.configuredWorldName(),
entry.storagePath());
case EMPTY -> MissingWorldStorageLog.warnEmptyOnce(
entry.configuredWorldName(),
entry.storagePath());
case UNUSABLE -> unusable.append(unusable.isEmpty() ? "" : "; ")
.append(entry.configuredWorldName())
.append(" (")
.append(entry.storagePath())
.append(": ")
.append(entry.detail() == null ? "storage is unusable" : entry.detail())
.append(')');
}
}
if (unusable.isEmpty()) {
return;
}
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) {
armStartupFailure(context, failure, LifecycleEvents.DATAPACK_DISCOVERY);
}
static <E extends LifecycleEvent> void armStartupFailure(
BootstrapContext context,
Throwable failure,
LifecycleEventType<? super BootstrapContext, ? extends E, ?> eventType
) {
context.getLifecycleManager().registerEventHandler(eventType, event -> {
throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure);
});
try {
context.getLogger().error(
"Iris bootstrap failed; registry and world startup will be stopped at datapack discovery.",
failure
);
} catch (Throwable loggingFailure) {
failure.addSuppressed(loggingFailure);
failure.printStackTrace(System.err);
}
}
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
try {
return DefaultPackBootstrapProvisioner.provision(
context.getDataDirectory(),
message -> context.getLogger().info(message),
startupPaths
);
} catch (IOException e) {
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
}
}
}
@@ -0,0 +1,69 @@
package art.arcane.iris;
import art.arcane.iris.util.common.misc.SlimJar;
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
@SuppressWarnings("UnstableApiUsage")
public final class IrisPluginLoader implements PluginLoader {
@Override
public void classloader(PluginClasspathBuilder classpathBuilder) {
PluginProviderContext context = classpathBuilder.getContext();
Path libraryRoot = context.getDataDirectory().resolve("cache").resolve("libraries");
SlimJar.loadBootstrap(libraryRoot, new SlimJar.BootstrapLogger() {
@Override
public void info(String message) {
context.getLogger().info(message);
}
@Override
public void error(String message) {
context.getLogger().error(message);
}
@Override
public void debug(String message) {
context.getLogger().debug(message);
}
});
for (Path library : relocatedLibraries(libraryRoot)) {
classpathBuilder.addLibrary(new JarLibrary(library));
}
}
static List<Path> relocatedLibraries(Path libraryRoot) {
try (Stream<Path> paths = Files.walk(libraryRoot)) {
List<Path> libraries = paths
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".jar"))
.filter(IrisPluginLoader::isRelocatedLibrary)
.sorted()
.toList();
if (libraries.isEmpty()) {
throw new IllegalStateException("Iris runtime library provisioning produced no relocated libraries.");
}
return libraries;
} catch (IOException failure) {
throw new IllegalStateException("Unable to enumerate provisioned Iris runtime libraries.", failure);
}
}
private static boolean isRelocatedLibrary(Path path) {
int count = path.getNameCount();
for (int i = 0; i < count - 2; i++) {
if ("relocated".equals(path.getName(i).toString())
&& "Iris".equals(path.getName(i + 1).toString())) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.api.pregen;
public enum IrisPregenPhase {
STARTED,
TICK,
PAUSED,
RESUMED,
SAVING,
COMPLETED,
CANCELLED
}
@@ -0,0 +1,31 @@
package art.arcane.iris.api.pregen;
import java.util.Objects;
public record IrisPregenProgress(
String worldName,
String worldIdentity,
double percent,
long generatedChunks,
long totalChunks,
long remainingChunks,
long failedChunks,
double chunksPerSecond,
long etaMillis,
long elapsedMillis,
String method,
boolean paused) {
public IrisPregenProgress {
Objects.requireNonNull(worldIdentity, "worldIdentity");
worldName = worldName == null ? worldIdentity : worldName;
method = method == null ? "" : method;
percent = Double.isFinite(percent) ? Math.clamp(percent, 0D, 100D) : 0D;
generatedChunks = Math.max(0L, generatedChunks);
totalChunks = Math.max(0L, totalChunks);
remainingChunks = Math.max(0L, remainingChunks);
failedChunks = Math.max(0L, failedChunks);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMillis = Math.max(0L, etaMillis);
elapsedMillis = Math.max(0L, elapsedMillis);
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.api.pregen;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
public class IrisPregenerationEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final IrisPregenPhase phase;
private final IrisPregenProgress progress;
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress) {
this.phase = Objects.requireNonNull(phase, "phase");
this.progress = Objects.requireNonNull(progress, "progress");
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public IrisPregenPhase getPhase() {
return phase;
}
public IrisPregenProgress getProgress() {
return progress;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
SURFACE_KIND,
BIOME_KEY
}
@@ -0,0 +1,61 @@
package art.arcane.iris.api.terrain;
import java.util.EnumSet;
import java.util.Objects;
public record IrisColumnQuery(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
public IrisColumnQuery {
Objects.requireNonNull(fields, "fields");
if (fields.isEmpty()) {
throw new IllegalArgumentException("at least one field is required");
}
if (maxBlockX < minBlockX || maxBlockZ < minBlockZ) {
throw new IllegalArgumentException("query bounds are inverted");
}
if (strideBlocks < 1) {
throw new IllegalArgumentException("strideBlocks must be at least 1");
}
fields = EnumSet.copyOf(fields);
}
public static IrisColumnQuery rect(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
return new IrisColumnQuery(minBlockX, minBlockZ, maxBlockX, maxBlockZ, strideBlocks, fields);
}
public long columnCount() {
long columnsX = (((long) maxBlockX - (long) minBlockX) / strideBlocks) + 1L;
long columnsZ = (((long) maxBlockZ - (long) minBlockZ) / strideBlocks) + 1L;
return saturatedProduct(columnsX, columnsZ);
}
public long chunkCount() {
long chunksX = ((long) (maxBlockX >> 4) - (long) (minBlockX >> 4)) + 1L;
long chunksZ = ((long) (maxBlockZ >> 4) - (long) (minBlockZ >> 4)) + 1L;
return saturatedProduct(chunksX, chunksZ);
}
private static long saturatedProduct(long left, long right) {
try {
return Math.multiplyExact(left, right);
} catch (ArithmeticException overflow) {
return Long.MAX_VALUE;
}
}
@Override
public EnumSet<IrisColumnField> fields() {
return EnumSet.copyOf(fields);
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
@@ -0,0 +1,9 @@
package art.arcane.iris.api.terrain;
public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
OCEAN,
VOID
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.terrain;
import org.bukkit.World;
import java.util.Optional;
import java.util.OptionalInt;
public interface IrisTerrainService {
boolean isIrisWorld(World world);
Optional<IrisWorldInfo> worldInfo(World world);
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
Optional<String> regionName(World world, int blockX, int blockZ);
int maxSampleColumns();
int maxSampleChunks();
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
}
@@ -0,0 +1,24 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisWorldInfo(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeight,
boolean studio) {
public IrisWorldInfo {
Objects.requireNonNull(dimensionKey, "dimensionKey");
Objects.requireNonNull(worldIdentity, "worldIdentity");
if (maxHeight <= minHeight) {
throw new IllegalArgumentException("maxHeight must exceed minHeight");
}
}
public int height() {
return maxHeight - minHeight;
}
}
@@ -0,0 +1,12 @@
package art.arcane.iris.api.tree;
import org.bukkit.block.Block;
import org.bukkit.event.block.BlockBreakEvent;
public interface IrisTreeFellerService {
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
boolean isManagedBreak(BlockBreakEvent event);
boolean isTreeBlock(Block block);
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.tree;
public enum TreeFellerAccess {
STANDALONE,
INTEGRATION_OVERRIDE
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.tree;
import java.util.Objects;
public record TreeFellerOptions(
TreeFellerAccess access,
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
public TreeFellerOptions {
Objects.requireNonNull(access, "access");
Objects.requireNonNull(runHooks, "runHooks");
if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) {
throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100");
}
}
public static TreeFellerOptions standalone() {
return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE);
}
public static TreeFellerOptions integrationOverride(
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
return new TreeFellerOptions(
TreeFellerAccess.INTEGRATION_OVERRIDE,
durabilityPreservationChance,
runHooks
);
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.api.tree;
public interface TreeFellerRunHooks {
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
@Override
public void onActivationAccepted() {
}
@Override
public boolean reserveLogCost() {
return true;
}
@Override
public void commitLogCost() {
}
@Override
public void refundLogCost() {
}
};
void onActivationAccepted();
boolean reserveLogCost();
void commitLogCost();
void refundLogCost();
}
@@ -0,0 +1,44 @@
package art.arcane.iris.api.world;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
import java.util.Optional;
public class IrisWorldEngineEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final World world;
private final IrisWorldPhase phase;
private final IrisWorldInfo info;
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info) {
this.world = Objects.requireNonNull(world, "world");
this.phase = Objects.requireNonNull(phase, "phase");
this.info = info;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public World getWorld() {
return world;
}
public IrisWorldPhase getPhase() {
return phase;
}
public Optional<IrisWorldInfo> getInfo() {
return Optional.ofNullable(info);
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.world;
public enum IrisWorldPhase {
ENGINE_READY,
ENGINE_HOTLOADED,
ENGINE_CLOSING
}
@@ -0,0 +1,794 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.Stream;
public final class BukkitWorldReconciler {
private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L;
private final Backend backend;
private final LifecycleOperationCoordinator coordinator;
public BukkitWorldReconciler(Iris plugin) {
this(new BukkitBackend(plugin), LifecycleOperationCoordinator.get());
}
BukkitWorldReconciler(Backend backend, LifecycleOperationCoordinator coordinator) {
this.backend = Objects.requireNonNull(backend, "backend");
this.coordinator = Objects.requireNonNull(coordinator, "coordinator");
}
public CompletableFuture<LoadResult> loadWorld(
File configurationFile,
String worldName
) {
NamespacedKey worldKey;
try {
worldKey = IrisWorldStorage.managedKeyFromName(worldName);
} catch (Throwable failure) {
return CompletableFuture.completedFuture(LoadResult.validationFailure(worldName, failure));
}
LifecycleOperationCoordinator.Lease lease;
try {
lease = acquireWorldLoad(worldKey);
} catch (LifecycleOperationCoordinator.BusyException failure) {
return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure));
}
DimensionResolution dimensionResolution;
Long configuredSeed;
String configuredWorldName;
try {
configuredWorldName = backend.configuredWorldName(worldKey);
dimensionResolution = backend.resolveDimension(worldKey);
configuredSeed = dimensionResolution.succeeded()
? backend.configuredSeed(configuredWorldName)
: null;
} catch (Throwable failure) {
dimensionResolution = DimensionResolution.failed(failure);
configuredSeed = null;
configuredWorldName = IrisWorldStorage.logicalName(worldKey);
}
if (!dimensionResolution.succeeded()) {
lease.close();
return CompletableFuture.completedFuture(LoadResult.dimensionFailure(
worldKey,
dimensionResolution.failure()));
}
return loadWithLease(
configurationFile,
configuredWorldName,
worldKey,
dimensionResolution.dimension(),
configuredSeed,
lease);
}
public CompletableFuture<BatchResult> checkForBukkitWorlds(Predicate<String> filter) {
Predicate<String> requiredFilter = Objects.requireNonNull(filter, "filter");
Map<String, String> configuredWorlds;
try {
configuredWorlds = backend.configuredWorlds();
} catch (Throwable failure) {
Iris.reportError("Failed while reading staged Bukkit worlds.", failure);
return CompletableFuture.completedFuture(new BatchResult(List.of(), failure));
}
CompletableFuture<List<LoadResult>> chain = CompletableFuture.completedFuture(new ArrayList<>());
for (Map.Entry<String, String> entry : configuredWorlds.entrySet()) {
String worldName = entry.getKey();
boolean selected;
try {
selected = requiredFilter.test(worldName);
} catch (Throwable failure) {
Iris.reportError("Failed while filtering staged Bukkit world \"" + worldName + "\".", failure);
return CompletableFuture.completedFuture(new BatchResult(List.of(), failure));
}
if (!selected) {
continue;
}
NamespacedKey worldKey;
try {
worldKey = backend.worldKeyFromConfiguration(worldName);
} catch (Throwable failure) {
chain = chain.thenApply(results -> {
results.add(LoadResult.validationFailure(worldName, failure));
return results;
});
continue;
}
String dimension = entry.getValue();
Long seed;
try {
seed = backend.configuredSeed(worldName);
} catch (Throwable failure) {
chain = chain.thenApply(results -> {
results.add(LoadResult.configurationFailure(worldKey, failure));
return results;
});
continue;
}
chain = chain.thenCompose(results -> loadConfiguredWorld(
ServerProperties.BUKKIT_YML,
worldName,
worldKey,
dimension,
seed)
.thenApply(result -> {
results.add(result);
return results;
}));
}
return chain.thenApply(results -> {
BatchResult batchResult = new BatchResult(results, null);
reportBatch(batchResult);
return batchResult;
});
}
private CompletableFuture<LoadResult> loadConfiguredWorld(
File configurationFile,
String configuredWorldName,
NamespacedKey worldKey,
String dimension,
Long seed
) {
LifecycleOperationCoordinator.Lease lease;
try {
lease = acquireWorldLoad(worldKey);
} catch (LifecycleOperationCoordinator.BusyException failure) {
return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure));
}
return loadWithLease(configurationFile, configuredWorldName, worldKey, dimension, seed, lease);
}
private LifecycleOperationCoordinator.Lease acquireWorldLoad(NamespacedKey worldKey) {
return coordinator.acquire(
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
LifecycleOperationCoordinator.OperationKind.WORLD_LOAD,
worldKey.toString());
}
private CompletableFuture<LoadResult> loadWithLease(
File configurationFile,
String configuredWorldName,
NamespacedKey worldKey,
String dimension,
Long seed,
LifecycleOperationCoordinator.Lease lease
) {
try {
IrisStartupValidation.requireWorldCreationReady();
backend.requireDimensionLoadable(worldKey, dimension);
} catch (Throwable failure) {
lease.close();
return CompletableFuture.completedFuture(LoadResult.dimensionFailure(worldKey, failure));
}
BukkitWorldConfiguration.Registration registration;
try {
registration = BukkitWorldConfiguration.register(
configurationFile,
configuredWorldName,
dimension,
seed);
} catch (Throwable failure) {
lease.close();
return CompletableFuture.completedFuture(LoadResult.configurationFailure(worldKey, failure));
}
CompletableFuture<ReconciliationResult> reconciliation;
try {
reconciliation = reconcile(worldKey, dimension, seed);
} catch (Throwable failure) {
reconciliation = CompletableFuture.completedFuture(ReconciliationResult.createFailure(worldKey, failure));
}
return reconciliation.handle((result, failure) -> {
ReconciliationResult settled = failure == null
? result
: ReconciliationResult.createFailure(worldKey, unwrap(failure));
if (settled == null) {
settled = ReconciliationResult.createFailure(
worldKey,
new IllegalStateException("World reconciliation completed without a result."));
}
if (settled.succeeded()
|| settled.status() == ReconciliationStatus.RESTART_REQUIRED
|| registration != BukkitWorldConfiguration.Registration.CREATED) {
return new LoadResult(settled, registration, false, true, null);
}
try {
boolean rolledBack = BukkitWorldConfiguration.removeIfMatching(
configurationFile,
configuredWorldName,
dimension,
seed);
return new LoadResult(settled, registration, true, rolledBack, null);
} catch (Throwable rollbackFailure) {
return new LoadResult(settled, registration, true, false, rollbackFailure);
}
})
.whenComplete((result, failure) -> lease.close());
}
private CompletableFuture<ReconciliationResult> reconcile(
NamespacedKey worldKey,
String dimension,
Long seed
) {
Optional<World> loaded = backend.loadedWorld(worldKey);
if (loaded.isPresent()) {
return CompletableFuture.completedFuture(verifyLoadedWorld(worldKey, loaded.get(), true));
}
CompletableFuture<World> created;
try {
created = Objects.requireNonNull(
backend.createWorld(worldKey, dimension, seed),
"World backend returned no creation future.");
} catch (Throwable failure) {
return CompletableFuture.completedFuture(classifyCreationFailure(worldKey, failure));
}
CompletableFuture<World> guardedCreation = guardCreateCompletion(
created,
worldKey,
TimeUnit.SECONDS.toMillis(WORLD_CREATE_TIMEOUT_SECONDS),
() -> ServerConfigurator.restart("World load timed out for \"" + worldKey + "\"."));
return guardedCreation.handle((createdWorld, failure) -> {
if (failure != null) {
return classifyCreationFailure(worldKey, unwrap(failure));
}
if (createdWorld == null) {
return ReconciliationResult.notLoaded(worldKey);
}
NamespacedKey createdKey;
try {
createdKey = WorldIdentity.key(createdWorld);
} catch (Throwable identityFailure) {
return ReconciliationResult.createFailure(worldKey, identityFailure);
}
if (!worldKey.equals(createdKey)) {
return ReconciliationResult.identityMismatch(worldKey, createdWorld, createdKey);
}
Optional<World> resolved = backend.loadedWorld(worldKey);
if (resolved.isEmpty()) {
return ReconciliationResult.notLoaded(worldKey);
}
return verifyLoadedWorld(worldKey, resolved.get(), false);
});
}
private ReconciliationResult verifyLoadedWorld(NamespacedKey worldKey, World loadedWorld, boolean alreadyLoaded) {
NamespacedKey loadedKey;
try {
loadedKey = WorldIdentity.key(loadedWorld);
} catch (Throwable identityFailure) {
return ReconciliationResult.createFailure(worldKey, identityFailure);
}
if (!worldKey.equals(loadedKey)) {
return ReconciliationResult.identityMismatch(worldKey, loadedWorld, loadedKey);
}
if (!backend.isIrisWorld(loadedWorld)) {
return ReconciliationResult.identityConflict(worldKey, loadedWorld);
}
return alreadyLoaded
? ReconciliationResult.alreadyLoaded(worldKey, loadedWorld)
: ReconciliationResult.loaded(worldKey, loadedWorld);
}
private static ReconciliationResult classifyCreationFailure(NamespacedKey worldKey, Throwable failure) {
Throwable cause = unwrap(failure);
if (cause instanceof TimeoutException || containsCreateWorldUnsupportedOperation(cause)) {
return ReconciliationResult.restartRequired(worldKey, cause);
}
return ReconciliationResult.createFailure(worldKey, cause);
}
static CompletableFuture<World> guardCreateCompletion(
CompletableFuture<World> source,
NamespacedKey worldKey,
long timeoutMillis,
Runnable timeoutAction
) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(worldKey, "worldKey");
Objects.requireNonNull(timeoutAction, "timeoutAction");
if (timeoutMillis < 1L) {
throw new IllegalArgumentException("timeoutMillis must be positive");
}
CompletableFuture<World> guarded = new CompletableFuture<>();
AtomicBoolean settled = new AtomicBoolean(false);
source.whenComplete((world, throwable) -> {
if (!settled.compareAndSet(false, true)) {
return;
}
if (throwable == null) {
guarded.complete(world);
} else {
guarded.completeExceptionally(unwrap(throwable));
}
});
CompletableFuture.delayedExecutor(timeoutMillis, TimeUnit.MILLISECONDS).execute(() -> {
if (!settled.compareAndSet(false, true)) {
return;
}
TimeoutException timeout = new TimeoutException(
"World load did not settle within " + timeoutMillis + " milliseconds for \""
+ worldKey + "\".");
try {
timeoutAction.run();
} catch (Throwable failure) {
timeout.addSuppressed(failure);
}
guarded.completeExceptionally(timeout);
});
return guarded;
}
private static void reportBatch(BatchResult batchResult) {
for (LoadResult result : batchResult.results()) {
if (result.succeeded()) {
Iris.info(C.LIGHT_PURPLE + result.message());
continue;
}
if (result.status() == ReconciliationStatus.BUSY) {
Iris.warn(result.message());
continue;
}
Iris.error(result.message());
Throwable failure = result.failure();
if (failure != null) {
Iris.reportError("Failed to reconcile staged world \"" + result.worldKey() + "\".", failure);
}
}
}
private static Throwable unwrap(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
current = current.getCause();
}
return current;
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
interface Backend {
Map<String, String> configuredWorlds();
Long configuredSeed(String worldName);
String configuredWorldName(NamespacedKey worldKey);
NamespacedKey worldKeyFromConfiguration(String configuredWorldName);
Optional<World> loadedWorld(NamespacedKey worldKey);
CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed);
boolean isIrisWorld(World world);
DimensionResolution resolveDimension(NamespacedKey worldKey);
void requireDimensionLoadable(NamespacedKey worldKey, String dimension);
}
public enum ReconciliationStatus {
LOADED,
ALREADY_LOADED,
BUSY,
INVALID_WORLD,
DIMENSION_UNRESOLVED,
CONFIGURATION_FAILED,
CREATE_FAILED,
RESTART_REQUIRED,
IDENTITY_MISMATCH,
IDENTITY_CONFLICT,
NOT_LOADED
}
public record ReconciliationResult(
ReconciliationStatus status,
NamespacedKey worldKey,
World world,
Throwable failure,
String message
) {
public ReconciliationResult {
Objects.requireNonNull(status, "status");
Objects.requireNonNull(message, "message");
}
public boolean succeeded() {
return status == ReconciliationStatus.LOADED || status == ReconciliationStatus.ALREADY_LOADED;
}
private static ReconciliationResult loaded(NamespacedKey worldKey, World world) {
return new ReconciliationResult(
ReconciliationStatus.LOADED,
worldKey,
world,
null,
"Loaded Iris world \"" + worldKey + "\".");
}
private static ReconciliationResult alreadyLoaded(NamespacedKey worldKey, World world) {
return new ReconciliationResult(
ReconciliationStatus.ALREADY_LOADED,
worldKey,
world,
null,
"Iris world \"" + worldKey + "\" is already loaded.");
}
private static ReconciliationResult createFailure(NamespacedKey worldKey, Throwable failure) {
return new ReconciliationResult(
ReconciliationStatus.CREATE_FAILED,
worldKey,
null,
failure,
"Failed to create Iris world \"" + worldKey + "\": " + failure.getMessage());
}
private static ReconciliationResult restartRequired(NamespacedKey worldKey, Throwable failure) {
return new ReconciliationResult(
ReconciliationStatus.RESTART_REQUIRED,
worldKey,
null,
failure,
"The server cannot load exact Iris world \"" + worldKey + "\" at this runtime phase.");
}
private static ReconciliationResult identityMismatch(
NamespacedKey worldKey,
World world,
NamespacedKey actualKey
) {
return new ReconciliationResult(
ReconciliationStatus.IDENTITY_MISMATCH,
worldKey,
world,
null,
"World creation returned \"" + actualKey + "\" instead of \"" + worldKey + "\".");
}
private static ReconciliationResult identityConflict(NamespacedKey worldKey, World world) {
return new ReconciliationResult(
ReconciliationStatus.IDENTITY_CONFLICT,
worldKey,
world,
null,
"World \"" + worldKey + "\" is loaded, but it is not an Iris world.");
}
private static ReconciliationResult notLoaded(NamespacedKey worldKey) {
return new ReconciliationResult(
ReconciliationStatus.NOT_LOADED,
worldKey,
null,
null,
"World creation completed without loading exact Iris world \"" + worldKey + "\".");
}
}
public record LoadResult(
ReconciliationResult reconciliation,
BukkitWorldConfiguration.Registration registration,
boolean rollbackAttempted,
boolean rollbackSucceeded,
Throwable rollbackFailure
) {
public LoadResult {
Objects.requireNonNull(reconciliation, "reconciliation");
}
public boolean succeeded() {
return reconciliation.succeeded() && rollbackFailure == null;
}
public ReconciliationStatus status() {
return reconciliation.status();
}
public NamespacedKey worldKey() {
return reconciliation.worldKey();
}
public World world() {
return reconciliation.world();
}
public Throwable failure() {
return rollbackFailure == null ? reconciliation.failure() : rollbackFailure;
}
public String message() {
if (rollbackFailure != null) {
return reconciliation.message() + " Failed to roll back bukkit.yml: " + rollbackFailure.getMessage();
}
if (rollbackAttempted && rollbackSucceeded) {
return reconciliation.message() + " The new bukkit.yml entry was rolled back.";
}
if (rollbackAttempted && !rollbackSucceeded) {
return reconciliation.message() + " The new bukkit.yml entry was no longer an exact match and was not modified.";
}
return reconciliation.message();
}
private static LoadResult validationFailure(String worldName, Throwable failure) {
ReconciliationResult reconciliation = new ReconciliationResult(
ReconciliationStatus.INVALID_WORLD,
null,
null,
failure,
"Invalid Iris world identifier \"" + worldName + "\": " + failure.getMessage());
return new LoadResult(reconciliation, null, false, true, null);
}
private static LoadResult busy(NamespacedKey worldKey, LifecycleOperationCoordinator.BusyException failure) {
ReconciliationResult reconciliation = new ReconciliationResult(
ReconciliationStatus.BUSY,
worldKey,
null,
failure,
failure.getMessage());
return new LoadResult(reconciliation, null, false, true, null);
}
private static LoadResult configurationFailure(NamespacedKey worldKey, Throwable failure) {
ReconciliationResult reconciliation = new ReconciliationResult(
ReconciliationStatus.CONFIGURATION_FAILED,
worldKey,
null,
failure,
"Failed to register Iris world \"" + worldKey + "\" in bukkit.yml: " + failure.getMessage());
return new LoadResult(reconciliation, null, false, true, null);
}
private static LoadResult dimensionFailure(NamespacedKey worldKey, Throwable failure) {
ReconciliationResult reconciliation = new ReconciliationResult(
ReconciliationStatus.DIMENSION_UNRESOLVED,
worldKey,
null,
failure,
"Could not determine one Iris dimension for world \"" + worldKey + "\": " + failure.getMessage());
return new LoadResult(reconciliation, null, false, true, null);
}
}
record DimensionResolution(String dimension, Throwable failure) {
DimensionResolution {
if ((dimension == null) == (failure == null)) {
throw new IllegalArgumentException("Dimension resolution must contain exactly one outcome.");
}
}
static DimensionResolution resolved(String dimension) {
return new DimensionResolution(Objects.requireNonNull(dimension, "dimension"), null);
}
static DimensionResolution failed(Throwable failure) {
return new DimensionResolution(null, Objects.requireNonNull(failure, "failure"));
}
boolean succeeded() {
return dimension != null;
}
}
public record BatchResult(List<LoadResult> results, Throwable failure) {
public BatchResult {
results = List.copyOf(Objects.requireNonNull(results, "results"));
}
public boolean succeeded() {
return failure == null && results.stream().allMatch(LoadResult::succeeded);
}
}
private static final class BukkitBackend implements Backend {
private final Iris plugin;
private BukkitBackend(Iris plugin) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
}
@Override
public Map<String, String> configuredWorlds() {
return new LinkedHashMap<>(IrisWorlds.readBukkitWorlds());
}
@Override
public Long configuredSeed(String worldName) {
return IrisWorlds.readBukkitWorldSeed(worldName);
}
@Override
public String configuredWorldName(NamespacedKey worldKey) {
return IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName());
}
@Override
public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) {
return IrisWorldStorage.keyFromConfiguredWorldName(
configuredWorldName,
IrisWorldStorage.levelRoot().getName()
);
}
@Override
public Optional<World> loadedWorld(NamespacedKey worldKey) {
return WorldIdentity.resolve(worldKey);
}
@Override
public CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed) {
try {
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
String configuredWorldName = configuredWorldName(worldKey);
Iris.info("Loading World: %s | Generator: %s", logicalWorldName, dimension);
ChunkGenerator generator = plugin.getDefaultWorldGenerator(configuredWorldName, dimension);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (generator == null || irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\".");
}
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + logicalWorldName + " using Iris:" + dimension + "...");
WorldCreator creator = WorldCreatorCompat.ofPersistentKey(worldKey)
.generator(generator)
.environment(BukkitEnvironment.from(irisDimension.getEnvironment()));
if (seed != null) {
creator.seed(seed);
}
return INMS.get().createWorldAsync(creator);
} catch (Throwable failure) {
return CompletableFuture.failedFuture(failure);
}
}
@Override
public boolean isIrisWorld(World world) {
return IrisToolbelt.isIrisWorld(world);
}
@Override
public DimensionResolution resolveDimension(NamespacedKey worldKey) {
File dimensionsDirectory = new File(snapshotRoot(worldKey), "dimensions");
if (!dimensionsDirectory.isDirectory()) {
return DimensionResolution.failed(new IllegalStateException("The world has no Iris dimensions directory."));
}
List<String> dimensions = new ArrayList<>();
Path dimensionsRoot = dimensionsDirectory.toPath().toAbsolutePath().normalize();
try (Stream<Path> paths = Files.walk(dimensionsRoot)) {
paths.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
.filter(path -> path.getFileName().toString().endsWith(".json"))
.forEach(path -> {
String relative = dimensionsRoot.relativize(path).toString().replace(File.separatorChar, '/');
dimensions.add(relative.substring(0, relative.length() - 5));
});
} catch (IOException failure) {
return DimensionResolution.failed(new IllegalStateException(
"The Iris dimensions directory could not be read.",
failure));
}
Collections.sort(dimensions);
String registeredDimension = IrisWorlds.get().getWorlds().get(worldKey.toString());
if (registeredDimension != null && dimensions.contains(registeredDimension)) {
return DimensionResolution.resolved(registeredDimension);
}
if (dimensions.size() == 1) {
return DimensionResolution.resolved(dimensions.getFirst());
}
if (dimensions.isEmpty()) {
return DimensionResolution.failed(new IllegalStateException("No dimension definitions were found."));
}
return DimensionResolution.failed(new IllegalStateException(
"Multiple dimension definitions were found without an exact registered dimension: "
+ String.join(", ", dimensions)));
}
@Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
String configuredWorldName = configuredWorldName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
}
}
private File snapshotRoot(NamespacedKey worldKey) {
File levelRoot = IrisWorldStorage.levelRoot();
File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName(worldKey),
worldKey
);
File expectedRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey);
if (!dimensionRoot.toPath().toAbsolutePath().normalize()
.equals(expectedRoot.toPath().toAbsolutePath().normalize())) {
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
}
}
}
@@ -0,0 +1,169 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.WorldInfo;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.Random;
final class IrisFailClosedChunkGenerator extends ChunkGenerator {
private final String refusalMessage;
private IrisFailClosedChunkGenerator(String refusalMessage) {
this.refusalMessage = Objects.requireNonNull(refusalMessage, "refusalMessage");
}
static IrisFailClosedChunkGenerator discoveryProbe(String worldName) {
return new IrisFailClosedChunkGenerator("Iris generator-discovery probe for '" + worldName
+ "' was asked to generate terrain. Iris worlds are created with /iris create.");
}
static IrisFailClosedChunkGenerator startupLock(String worldName, String denialReason) {
return new IrisFailClosedChunkGenerator("Iris generation for '" + worldName
+ "' remains locked: " + denialReason);
}
@Override
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
throw refusal();
}
@Override
public void generateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
throw refusal();
}
@Override
public void generateBedrock(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
throw refusal();
}
@Override
public void generateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) {
throw refusal();
}
@Override
public ChunkData generateChunkData(@NotNull World world, @NotNull Random random, int x, int z, @NotNull BiomeGrid biome) {
throw refusal();
}
@Override
public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) {
throw refusal();
}
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
throw refusal();
}
@Override
public boolean canSpawn(@NotNull World world, int x, int z) {
throw refusal();
}
@Override
public List<BlockPopulator> getDefaultPopulators(@NotNull World world) {
throw refusal();
}
@Override
public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) {
throw refusal();
}
@Override
public boolean shouldGenerateNoise() {
return false;
}
@Override
public boolean shouldGenerateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateSurface() {
return false;
}
@Override
public boolean shouldGenerateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateBedrock() {
return false;
}
@Override
public boolean shouldGenerateCaves() {
return false;
}
@Override
public boolean shouldGenerateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateDecorations() {
return false;
}
@Override
public boolean shouldGenerateDecorations(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateMobs() {
return false;
}
@Override
public boolean shouldGenerateMobs(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateStructures() {
return false;
}
@Override
public boolean shouldGenerateStructures(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
private IllegalStateException refusal() {
return new IllegalStateException(refusalMessage);
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.core;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
public final class IrisStartupAdmissionListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
IrisStartupValidation.denialReason().ifPresent(reason -> event.disallow(
AsyncPlayerPreLoginEvent.Result.KICK_OTHER,
reason + " Check the server console, correct the reported Iris state, and restart."));
}
}
@@ -0,0 +1,505 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
* Bukkit plugin entry points delegate to.
*/
public final class IrisWorldGeneratorResolver {
private static final int VALIDATION_STABILITY_ATTEMPTS = 2;
private static final Object SNAPSHOT_VALIDATION_LOCK = new Object();
private static final String IRIS_DIMENSION_NAMESPACE = "iris";
private static final String PLOT_SQUARED_DISCOVERY_WORLD = "CheckingPlotSquaredGenerator";
private final VolmitPlugin plugin;
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
this.plugin = plugin;
}
public void validateAllPacks() {
File packsRoot = plugin.getDataFolder("packs");
List<File> packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
PackValidationRegistry.clear();
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
ServerConfigurator.PackContentSnapshot contentSnapshot =
new ServerConfigurator.PackContentSnapshot("", Map.of());
String contextFingerprint = "";
Optional<List<PackValidationResult>> cached = Optional.empty();
try {
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
contextFingerprint = PackValidationCache.contextFingerprint();
cached = PackValidationCache.load(
cacheFile,
contentSnapshot.content(),
contextFingerprint,
packNames);
} catch (RuntimeException exception) {
Iris.reportError("Could not evaluate the persisted pack-validation cache", exception);
}
List<PackValidationResult> results;
if (cached.isPresent()) {
results = cached.get();
Iris.info("Reused persisted validation for " + results.size()
+ " unchanged Iris pack(s); full pack parsing was skipped.");
} else {
FreshValidation validation = validateStablePacks(packsRoot, packDirs, contentSnapshot);
packDirs = validation.packDirs();
contentSnapshot = validation.contentSnapshot();
results = validation.results();
if (validation.stable()) {
try {
PackValidationCache.save(
cacheFile,
contentSnapshot.content(),
contextFingerprint,
results);
} catch (IOException exception) {
Iris.reportError("Could not persist Iris pack-validation results", exception);
}
}
}
Map<String, String> packFingerprints = contentSnapshot.packContents();
for (PackValidationResult result : results) {
PackValidationRegistry.publish(result);
String packFingerprint = packFingerprints.get(result.getPackName());
File packDirectory = PackDirectoryResolver.resolveExisting(packsRoot, result.getPackName());
if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) {
PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint);
}
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName()
+ "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else if (cached.isEmpty()) {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
}
IrisStartupValidation.markPacksReady();
}
private static FreshValidation validateStablePacks(
File packsRoot,
List<File> initialPackDirs,
ServerConfigurator.PackContentSnapshot initialSnapshot
) {
List<File> packDirs = initialPackDirs;
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
List<PackValidationResult> results = validatePacks(packDirs);
ServerConfigurator.PackContentSnapshot after;
try {
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
} catch (RuntimeException exception) {
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
}
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
if (!before.content().isBlank()
&& before.content().equals(after.content())
&& packNames.equals(afterPackNames)) {
return new FreshValidation(afterPackDirs, after, results, true);
}
packDirs = afterPackDirs;
before = after;
}
Iris.error("Iris pack files kept changing during validation; validation was refused until writes stop.");
List<PackValidationResult> failures = new ArrayList<>(packDirs.size());
for (File packDir : packDirs) {
failures.add(new PackValidationResult(
packDir.getName(),
List.of("Pack files changed while validation was in progress; retry after writes stop."),
List.of(),
System.currentTimeMillis()));
}
return new FreshValidation(packDirs, before, failures, false);
}
private static List<PackValidationResult> validatePacks(List<File> packDirs) {
List<PackValidationResult> results = new ArrayList<>(packDirs.size());
for (File packDir : packDirs) {
try {
results.add(PackValidator.validate(packDir));
} catch (Throwable exception) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", exception);
String detail = exception.getMessage();
if (detail == null || detail.isBlank()) {
detail = exception.getClass().getSimpleName();
}
results.add(new PackValidationResult(
packDir.getName(),
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
+ ": " + detail),
List.of(),
System.currentTimeMillis()));
}
}
return results;
}
static PackValidationResult requireSnapshotLoadable(File packRoot) {
Path normalizedRoot = packRoot.toPath().toAbsolutePath().normalize();
PackValidationResult result = PackValidationRegistry.get(normalizedRoot);
if (result == null) {
synchronized (SNAPSHOT_VALIDATION_LOCK) {
result = PackValidationRegistry.get(normalizedRoot);
if (result == null) {
PackValidationRegistry.ValidationTicket ticket =
PackValidationRegistry.tryBeginValidation(normalizedRoot);
if (ticket != null) {
try {
result = PackValidator.validate(normalizedRoot.toFile());
} catch (Throwable exception) {
Iris.reportError("Snapshot pack validation failed for '" + normalizedRoot + "'", exception);
String detail = exception.getMessage();
if (detail == null || detail.isBlank()) {
detail = exception.getClass().getSimpleName();
}
result = new PackValidationResult(
normalizedRoot.getFileName().toString(),
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
+ ": " + detail),
List.of(),
System.currentTimeMillis());
}
PackValidationRegistry.publishIfCurrent(ticket, result);
}
}
}
}
return PackValidationRegistry.requireLoadable(normalizedRoot);
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
File pack = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName,
worldKey
)
.map(IrisWorldStorage::requireFrozenPackRoot)
.orElse(null);
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
if (PackDownloader.isPackPresent(packsRoot, id)) {
Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
return null;
}
Iris.warn("Unable to find dimension type " + id + ". Install its pack with "
+ PackDownloader.downloadCommandFor(id) + " and restart the server.");
}
return dimension;
}
/**
* Resolves the Iris key behind a Bukkit world name, accepting the runtime keyed name alongside the
* startup name.
* <p>
* Paper names a world built from {@code WorldCreator.ofKey} {@code <namespace>_<key>}, and Multiverse
* loads Iris worlds that way because its own keyed creator refuses a non-{@code minecraft} namespace.
* That is not the startup name, so the configured-name parser reads {@code iris_moon} as
* {@code iris:iris_moon} and loses the world.
* <p>
* Only a name whose decoded key already has Iris storage is remapped, and only when the literal
* reading has none, so a world genuinely created as {@code /iris create "iris moon"} keeps its own
* identity and a server without Multiverse resolves exactly the names it resolved before.
*/
static NamespacedKey configuredWorldKey(String worldName, String levelName, File levelRoot) {
NamespacedKey literal = IrisWorldStorage.keyFromConfiguredWorldName(worldName, levelName);
String runtimePrefix = IRIS_DIMENSION_NAMESPACE + "_";
if (!worldName.startsWith(runtimePrefix)
|| worldName.startsWith(levelName + "_" + runtimePrefix)
|| IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, literal)) {
return literal;
}
NamespacedKey runtime;
try {
runtime = IrisWorldStorage.managedKeyFromName(
IRIS_DIMENSION_NAMESPACE + ":" + worldName.substring(runtimePrefix.length()),
levelName);
} catch (RuntimeException notAManagedKey) {
return literal;
}
if (!IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, runtime)) {
return literal;
}
IrisLogging.debug("Resolved runtime keyed world name " + worldName + " as " + runtime);
return runtime;
}
/**
* The key a refusal message should name for a world name, without the storage-existence guard.
* <p>
* {@link #configuredWorldKey} only remaps the runtime keyed name when the decoded key has storage, so an
* orphan keeps the literal reading and a message built from it names {@code iris:iris_moon} and tells
* the admin to create a world that never existed under that name. Messaging does not have to be safe
* against mis-mapping a live world, only correct about what the admin typed, so it drops that guard;
* generation keeps it.
*/
static NamespacedKey messageWorldKey(String worldName, String levelName) {
NamespacedKey literal = IrisWorldStorage.keyFromConfiguredWorldName(worldName, levelName);
String runtimePrefix = IRIS_DIMENSION_NAMESPACE + "_";
if (!worldName.startsWith(runtimePrefix)
|| worldName.startsWith(levelName + "_" + runtimePrefix)) {
return literal;
}
try {
return IrisWorldStorage.managedKeyFromName(
IRIS_DIMENSION_NAMESPACE + ":" + worldName.substring(runtimePrefix.length()),
levelName);
} catch (RuntimeException notAManagedKey) {
return literal;
}
}
/**
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
* Iris has nothing staged.
*/
@Nullable
public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier<BiomeProvider> fallback) {
BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
}
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
return fallback.get();
}
@Nullable
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, @Nullable String id) {
if (isPlotSquaredGeneratorDiscoveryProbe(worldName, id)) {
Iris.debug("Ignoring PlotSquared generator discovery probe");
return null;
}
if (isGeneratorDiscoveryProbe(worldName, id)) {
Iris.debug("Generator discovery probe for loaded world " + worldName);
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
}
Optional<String> startupDenial = IrisStartupValidation.denialReason();
if (startupDenial.isPresent()) {
Iris.warn("Keeping configured Iris world '" + worldName
+ "' generation-locked: " + startupDenial.get());
return IrisFailClosedChunkGenerator.startupLock(worldName, startupDenial.get());
}
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
}
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
requireWorldKeyAvailable(worldName, worldKey);
requireOwnedWorld(worldName, levelRoot, worldKey);
try {
return resolveFrozenWorldGenerator(worldName, id);
} catch (RuntimeException failure) {
Iris.reportError("Refusing to load configured Iris world '" + worldName
+ "' because its frozen world-local pack snapshot could not be used.", failure);
Bukkit.shutdown();
throw failure;
}
}
private static boolean isPlotSquaredGeneratorDiscoveryProbe(String worldName, String id) {
return PLOT_SQUARED_DISCOVERY_WORLD.equals(worldName) && id != null && id.isEmpty();
}
/**
* Multiverse-Core probes every enabled plugin by asking for a generator with an empty dimension
* id and the name of a world that is already loaded. Bukkit never creates a world that is
* already loaded, so that pair only ever means discovery, never generation.
*/
private static boolean isGeneratorDiscoveryProbe(String worldName, String id) {
return id != null && id.isEmpty() && Bukkit.getWorld(worldName) != null;
}
/**
* Refuses a second generator for a world key that is already live. Multiverse imports accept a
* world name that maps onto a loaded Iris key, which would otherwise start a second engine on
* the same storage with a different seed.
*/
private static void requireWorldKeyAvailable(String worldName, NamespacedKey worldKey) {
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
if (loaded == null) {
return;
}
throw new IllegalStateException("Refusing to generate '" + worldName + "': " + worldKey
+ " is already loaded as '" + loaded.getName() + "'.");
}
/**
* Refuses worlds Iris does not own before the fail-fast path can see them. A world without Iris
* storage is somebody else's create or import, and returning null there would silently hand the
* caller a vanilla world.
* <p>
* The {@code iris} dimension namespace is Iris-exclusive, so a directory in it is ownership on
* its own. Every server already has {@code dimensions/minecraft/*}, so a vanilla slot counts as
* Iris' only once it carries a frozen pack snapshot.
*/
private static void requireOwnedWorld(String worldName, File levelRoot, NamespacedKey worldKey) {
File dimensionRoot;
try {
dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
worldName,
worldKey
).orElse(null);
} catch (RuntimeException unusableStorage) {
// Storage exists but cannot be resolved: an owned world, left to the fail-fast path.
return;
}
if (dimensionRoot != null
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) {
return;
}
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
throw new IllegalStateException("'" + worldName + "' (" + messageKey
+ ") has no Iris world storage, so Iris cannot generate it."
+ " Create Iris worlds with /iris create " + IrisWorldStorage.logicalName(messageKey)
+ " type=<pack>; Iris registers them with Multiverse itself.");
}
private static boolean hasFrozenPack(File dimensionRoot) {
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack");
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS);
}
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
worldName,
worldKey
);
File expectedDimensionRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey);
if (!dimensionRoot.toPath().toAbsolutePath().normalize()
.equals(expectedDimensionRoot.toPath().toAbsolutePath().normalize())) {
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
try {
requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) {
Iris.error("Refusing to create world '" + worldName + "' using broken snapshot at '"
+ snapshotRoot + "':");
for (String reason : exception.getReasons()) {
Iris.error(" - " + reason);
}
throw exception;
}
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
if (dimension == null) {
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
+ " does not contain dimension " + id + ".");
}
Iris.debug("Assuming IrisDimension: " + dimension.getName());
IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(dimensionRoot)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
Iris.debug("Generator Config: " + world);
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
}
private record FreshValidation(
List<File> packDirs,
ServerConfigurator.PackContentSnapshot contentSnapshot,
List<PackValidationResult> results,
boolean stable
) {
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.core;
import io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.util.Objects;
import java.util.UUID;
public final class PaperWorldReplacementEntryListener implements Listener {
private final PendingWorldReplacementManager manager;
public PaperWorldReplacementEntryListener(PendingWorldReplacementManager manager) {
this.manager = Objects.requireNonNull(manager, "manager");
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event) {
UUID playerId = event.getConnection().getProfile().getId();
if (playerId == null) {
return;
}
try {
PendingWorldReplacementManager.ReplacementEntryRedirect redirect = manager.prepareReplacementEntry(
playerId,
event.getSpawnLocation(),
event.isNewPlayer()
);
if (redirect == null) {
return;
}
Location location = redirect.location();
event.setSpawnLocation(location);
if (redirect.acknowledgementRequired()) {
manager.expectReplacementEntryAcknowledgement(playerId, redirect.transactionId());
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
refuseUnsafeEntry(event, playerId, failure);
} catch (Throwable failure) {
refuseUnsafeEntry(event, playerId, failure);
}
}
private void refuseUnsafeEntry(AsyncPlayerSpawnLocationEvent event, UUID playerId, Throwable failure) {
manager.reportUnsafeEntry(playerId, failure);
event.getConnection().disconnect(Component.text(
"Iris could not verify a safe login location after the Overworld replacement. Retry after startup completes."
));
}
}
@@ -0,0 +1,548 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.runtime.WorldDeletionQueue;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private static final String EXACT_PREFIX = "exact:";
private static final Pattern SAFE_LOGICAL_NAME = Pattern.compile("^[a-z0-9_-]+$");
private static final Pattern QUARANTINE_NAME = Pattern.compile("^\\.iris-delete-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
private static final Set<String> VANILLA_DIMENSION_ALIASES = Set.of("overworld", "the_nether", "the_end");
private final VolmitPlugin plugin;
public PendingWorldDeleteQueue(VolmitPlugin plugin) {
this.plugin = plugin;
}
@Override
public synchronized int queueExactForStartupDeletion(Collection<String> worldNames) throws IOException {
return queueWorldDeletionOnStartup(worldNames, true);
}
@Override
public synchronized int queueFamilyForStartupDeletion(Collection<String> worldNames) throws IOException {
return queueWorldDeletionOnStartup(worldNames, false);
}
private int queueWorldDeletionOnStartup(Collection<String> worldNames, boolean exact) throws IOException {
if (worldNames == null || worldNames.isEmpty()) {
return 0;
}
File levelRoot = IrisWorldStorage.levelRoot();
ArrayList<String> normalizedNames = new ArrayList<>(worldNames.size());
for (String worldName : worldNames) {
String normalized = normalizeQueueEntry(worldName, levelRoot.getName());
if (normalized == null) {
throw new IllegalArgumentException("Unsafe Iris world deletion target: " + worldName);
}
normalizedNames.add(exact && !QUARANTINE_NAME.matcher(normalized).matches()
? EXACT_PREFIX + normalized
: normalized);
}
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap(queueFile, levelRoot.getName());
int before = queue.size();
for (String normalized : normalizedNames) {
mergeQueueEntry(queue, normalized);
}
if (queue.size() != before) {
writePendingWorldDeleteMap(queueFile, queue);
}
return queue.size() - before;
}
public synchronized void processPendingStartupWorldDeletes() {
try {
unregisterTransientStudioWorlds();
File levelRoot = IrisWorldStorage.levelRoot();
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap(queueFile, levelRoot.getName());
for (String discoveredName : discoverStartupWorldNames(levelRoot)) {
mergeQueueEntry(queue, discoveredName);
}
if (queue.isEmpty()) {
if (queueFile.exists()) {
writePendingWorldDeleteMap(queueFile, queue);
}
return;
}
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
for (String worldName : queue.values()) {
processEntry(levelRoot, worldName, remaining);
}
writePendingWorldDeleteMap(queueFile, remaining);
} catch (Throwable failure) {
Iris.reportError("Failed to process queued startup world deletions.", failure);
}
}
@Nullable
static String normalizeQueueEntry(String worldName, String levelName) {
if (worldName == null) {
return null;
}
String candidate = worldName.trim();
if (candidate.isEmpty()) {
return null;
}
if (QUARANTINE_NAME.matcher(candidate).matches()) {
return candidate;
}
String logicalName = candidate.startsWith("iris:") ? candidate.substring("iris:".length()) : candidate;
if (!SAFE_LOGICAL_NAME.matcher(logicalName).matches()) {
return null;
}
String normalizedLevelName = Objects.requireNonNull(levelName, "levelName").trim().toLowerCase(Locale.ROOT);
if (VANILLA_DIMENSION_ALIASES.contains(logicalName)
|| logicalName.equals(normalizedLevelName)
|| logicalName.equals(normalizedLevelName + "_nether")
|| logicalName.equals(normalizedLevelName + "_the_end")) {
return null;
}
try {
NamespacedKey key = IrisWorldStorage.managedKeyFromName(candidate, normalizedLevelName);
return key.getKey().equals(logicalName) ? logicalName : null;
} catch (IllegalArgumentException failure) {
return null;
}
}
static LinkedHashMap<String, String> loadPendingWorldDeleteMap(File queueFile, String levelName) throws IOException {
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
Path queuePath = Objects.requireNonNull(queueFile, "queueFile").toPath();
if (!Files.exists(queuePath, LinkOption.NOFOLLOW_LINKS)) {
return queue;
}
try (BufferedReader reader = Files.newBufferedReader(queuePath, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
String normalized = normalizeStoredQueueEntry(line, levelName);
if (normalized != null) {
mergeQueueEntry(queue, normalized);
}
}
}
return queue;
}
@Nullable
private static String normalizeStoredQueueEntry(String storedEntry, String levelName) {
if (storedEntry == null) {
return null;
}
String candidate = storedEntry.trim();
boolean exact = candidate.startsWith(EXACT_PREFIX);
String rawName = exact ? candidate.substring(EXACT_PREFIX.length()) : candidate;
String normalized = normalizeQueueEntry(rawName, levelName);
if (normalized == null || QUARANTINE_NAME.matcher(normalized).matches()) {
return normalized;
}
return exact ? EXACT_PREFIX + normalized : normalized;
}
private static void mergeQueueEntry(Map<String, String> queue, String storedEntry) {
String logicalKey = storedEntry.startsWith(EXACT_PREFIX)
? storedEntry.substring(EXACT_PREFIX.length())
: storedEntry;
String key = logicalKey.toLowerCase(Locale.ROOT);
String existing = queue.get(key);
if (existing == null || (existing.startsWith(EXACT_PREFIX) && !storedEntry.startsWith(EXACT_PREFIX))) {
queue.put(key, storedEntry);
}
}
static void writePendingWorldDeleteMap(File queueFile, Map<String, String> queue) throws IOException {
Path queuePath = Objects.requireNonNull(queueFile, "queueFile").toPath().toAbsolutePath().normalize();
Path parent = queuePath.getParent();
if (parent == null) {
throw new IOException("Queue file has no parent directory: " + queuePath);
}
Files.createDirectories(parent);
StringBuilder content = new StringBuilder();
for (String worldName : Objects.requireNonNull(queue, "queue").values()) {
content.append(worldName).append('\n');
}
Path temporary = parent.resolve(queuePath.getFileName() + ".tmp-" + UUID.randomUUID());
IOException failure = null;
try {
byte[] bytes = content.toString().getBytes(StandardCharsets.UTF_8);
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
)) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
replaceQueueFile(temporary, queuePath);
forceDirectory(parent);
} catch (IOException writeFailure) {
failure = writeFailure;
throw writeFailure;
} finally {
try {
Files.deleteIfExists(temporary);
} catch (IOException cleanupFailure) {
if (failure != null) {
failure.addSuppressed(cleanupFailure);
} else {
throw cleanupFailure;
}
}
}
}
static LinkedHashSet<String> discoverStartupWorldNames(File levelRoot) throws IOException {
LinkedHashSet<String> worldNames = new LinkedHashSet<>();
Path root = Objects.requireNonNull(levelRoot, "levelRoot").toPath().toAbsolutePath().normalize();
Path dimensions = root.resolve("dimensions");
Path irisNamespace = dimensions.resolve("iris");
if (!Files.exists(irisNamespace, LinkOption.NOFOLLOW_LINKS)) {
return worldNames;
}
if (Files.isSymbolicLink(dimensions) || Files.isSymbolicLink(irisNamespace)) {
throw new IOException("Iris dimension storage contains a symbolic link: " + irisNamespace);
}
if (!Files.isDirectory(irisNamespace, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Iris dimension storage is not a directory: " + irisNamespace);
}
try (DirectoryStream<Path> children = Files.newDirectoryStream(irisNamespace)) {
for (Path child : children) {
if (Files.isSymbolicLink(child) || !Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
String name = child.getFileName().toString();
if (QUARANTINE_NAME.matcher(name).matches()) {
worldNames.add(name);
continue;
}
String transientBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(name);
String normalized = normalizeQueueEntry(transientBaseName, root.getFileName().toString());
if (normalized != null) {
worldNames.add(normalized);
}
}
}
return worldNames;
}
static List<Path> resolveQueueEntryPaths(File levelRoot, String worldName) throws IOException {
QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName());
List<DeleteTarget> targets = entry.targets(levelRoot);
ArrayList<Path> paths = new ArrayList<>(targets.size());
for (DeleteTarget target : targets) {
paths.add(target.path());
}
return List.copyOf(paths);
}
private static void replaceQueueFile(Path temporary, Path queuePath) throws IOException {
try {
Files.move(temporary, queuePath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException unsupported) {
Files.move(temporary, queuePath, StandardCopyOption.REPLACE_EXISTING);
}
}
private static void forceDirectory(Path directory) throws IOException {
// A directory can only be opened and fsynced on a POSIX filesystem; Windows rejects the
// open outright. Matches DirectoryDurability and DatapackIngestService, which already
// skip the barrier there.
if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
}
private static void unregisterTransientStudioWorlds() {
try {
int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml();
if (unregistered > 0) {
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
}
} catch (Throwable failure) {
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", failure);
}
}
private static void processEntry(
File levelRoot,
String worldName,
LinkedHashMap<String, String> remaining
) {
try {
EntryDeletionResult result = attemptEntry(
levelRoot,
worldName,
PendingWorldDeleteQueue::isLoaded
);
if (result.loaded()) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
return;
}
for (Path deleted : result.deleted()) {
Iris.info("Deleted queued world folder \"" + deleted.getFileName() + "\".");
}
for (DeletionFailure deletionFailure : result.failures()) {
Iris.reportError(
"Failed to delete queued world folder \"" + deletionFailure.path() + "\".",
deletionFailure.failure()
);
}
if (!result.foundAny()) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
return;
}
if (result.retainQueueEntry()) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
}
} catch (Throwable failure) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
Iris.reportError("Failed to safely process queued world deletion for \"" + worldName + "\".", failure);
}
}
static EntryDeletionResult attemptEntry(
File levelRoot,
String worldName,
LoadedTargetCheck loadedTargetCheck
) throws IOException {
QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName());
List<DeleteTarget> targets = entry.targets(levelRoot);
if (targets.stream().anyMatch(target -> loadedTargetCheck.isLoaded(target.key(), target.path()))) {
return new EntryDeletionResult(true, false, List.of(), List.of());
}
boolean foundAny = false;
ArrayList<Path> deleted = new ArrayList<>(targets.size());
ArrayList<DeletionFailure> failures = new ArrayList<>(targets.size());
for (DeleteTarget target : targets) {
Path worldFolder = target.path();
if (!Files.exists(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
if (Files.isSymbolicLink(worldFolder) || !Files.isDirectory(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Queued world target is not a safe directory: " + worldFolder);
}
foundAny = true;
try {
SnapshotDirectoryTreeDeleter.delete(worldFolder);
deleted.add(worldFolder);
} catch (IOException failure) {
failures.add(new DeletionFailure(worldFolder, failure));
}
}
return new EntryDeletionResult(
false,
foundAny,
List.copyOf(deleted),
List.copyOf(failures)
);
}
private static boolean isLoaded(@Nullable NamespacedKey key, Path path) {
if (key != null && WorldIdentity.resolve(key).isPresent()) {
return true;
}
Path targetPath = path.toAbsolutePath().normalize();
for (World world : Bukkit.getWorlds()) {
if (world.getWorldFolder().toPath().toAbsolutePath().normalize().equals(targetPath)) {
return true;
}
}
return false;
}
private enum QueueEntryType {
EXACT,
LOGICAL,
QUARANTINE
}
private record QueueEntry(String storedName, QueueEntryType type) {
private static QueueEntry parse(String worldName, String levelName) {
String stored = normalizeStoredQueueEntry(worldName, levelName);
if (stored == null) {
throw new IllegalArgumentException("Unsafe queued Iris world deletion target: " + worldName);
}
boolean exact = stored.startsWith(EXACT_PREFIX);
String normalized = exact ? stored.substring(EXACT_PREFIX.length()) : stored;
QueueEntryType type = QUARANTINE_NAME.matcher(normalized).matches()
? QueueEntryType.QUARANTINE
: exact ? QueueEntryType.EXACT : QueueEntryType.LOGICAL;
return new QueueEntry(normalized, type);
}
private List<DeleteTarget> targets(File levelRoot) throws IOException {
if (type == QueueEntryType.QUARANTINE) {
return List.of(new DeleteTarget(null, requireSafeQuarantinePath(levelRoot, storedName)));
}
if (type == QueueEntryType.EXACT) {
NamespacedKey key = IrisWorldStorage.managedKeyFromName(storedName, levelRoot.getName());
Path path = currentStorageRoot(levelRoot, key);
return List.of(new DeleteTarget(key, path));
}
ArrayList<DeleteTarget> targets = new ArrayList<>(3);
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(storedName)) {
NamespacedKey key = IrisWorldStorage.managedKeyFromName(familyWorldName, levelRoot.getName());
Path path = currentStorageRoot(levelRoot, key);
targets.add(new DeleteTarget(key, path));
}
return targets;
}
private static Path currentStorageRoot(File levelRoot, NamespacedKey key) {
File worldContainer = levelRoot.getAbsoluteFile().getParentFile();
if (worldContainer == null) {
throw new IllegalArgumentException("Selected level root has no world container: " + levelRoot);
}
String configuredWorldName = IrisWorldStorage.configuredWorldName(key, levelRoot.getName());
Path directRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(levelRoot, key)
.toPath()
.toAbsolutePath()
.normalize();
Path dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
worldContainer,
levelRoot,
configuredWorldName,
key
).map(file -> file.toPath().toAbsolutePath().normalize()).orElse(directRoot);
if (dimensionRoot.equals(directRoot)) {
return directRoot;
}
Path configuredDimensionRoot = IrisWorldStorage.configuredDimensionRoot(
worldContainer,
levelRoot,
key
).toPath().toAbsolutePath().normalize();
if (!dimensionRoot.equals(configuredDimensionRoot)) {
throw new IllegalStateException("Iris world storage does not match the current platform layout.");
}
return IrisWorldStorage.configuredLevelRoot(worldContainer, levelRoot, key)
.toPath()
.toAbsolutePath()
.normalize();
}
}
private static Path requireSafeQuarantinePath(File levelRoot, String quarantineName) throws IOException {
if (!QUARANTINE_NAME.matcher(quarantineName).matches()) {
throw new IOException("Invalid Iris quarantine directory name: " + quarantineName);
}
Path root = levelRoot.toPath().toAbsolutePath().normalize();
Path dimensions = root.resolve("dimensions");
Path irisNamespace = dimensions.resolve("iris");
Path target = irisNamespace.resolve(quarantineName).normalize();
if (!Objects.equals(target.getParent(), irisNamespace)) {
throw new IOException("Iris quarantine target escapes its namespace: " + target);
}
for (Path path : List.of(dimensions, irisNamespace, target)) {
if (Files.isSymbolicLink(path)) {
throw new IOException("Iris quarantine storage contains a symbolic link: " + path);
}
}
return target;
}
private record DeleteTarget(@Nullable NamespacedKey key, Path path) {
}
@FunctionalInterface
interface LoadedTargetCheck {
boolean isLoaded(@Nullable NamespacedKey key, Path path);
}
record EntryDeletionResult(
boolean loaded,
boolean foundAny,
List<Path> deleted,
List<DeletionFailure> failures
) {
boolean retainQueueEntry() {
return loaded || !failures.isEmpty();
}
}
record DeletionFailure(Path path, IOException failure) {
}
}
@@ -0,0 +1,75 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
public class CommandDatapack implements DirectorExecutor {
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
public void ingest(
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
boolean restart
) {
VolmitSender sender = sender();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_STARTING_DATAPACK_INGEST));
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
}
@Director(description = "List configured datapack imports and their installed versions", descriptionKey = "iris.director.commanddatapack.director.list_configured_datapack_imports_their_installed_versions", aliases = {"ls"})
public void list() {
VolmitSender sender = sender();
KList<String> configured = DatapackIngestService.collectConfiguredImports();
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_CONFIGURED_DATAPACK_IMPORTS, MessageArgument.untrusted("value", configured.size())));
for (String url : configured) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE, MessageArgument.untrusted("url", url)));
}
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_INSTALLED_DATAPACKS, MessageArgument.untrusted("value", installed.size())));
for (DatapackIngestService.Entry entry : installed) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE_2, MessageArgument.untrusted("value", entry.id), MessageArgument.untrusted("value2", (entry.versionNumber == null ? "?" : entry.versionNumber))));
}
if (configured.isEmpty()) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_ADD_MODRINTH_URLS_DIMENSION_S_DATAPACKIMPORTS_LIST_THEN_RUN_IRIS));
}
}
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", descriptionKey = "iris.director.commanddatapack.director.remove_installed_datapack_by_id_also_delete_its_url_from_datapackimports_keep", aliases = {"rm"})
public void remove(
@Param(description = "The datapack id (folder name) shown by /iris datapack list", descriptionKey = "iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list")
String id
) {
DatapackIngestService.remove(sender(), id);
}
}
@@ -0,0 +1,420 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import com.google.gson.JsonObject;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.runtime.ChunkClearer;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisPackBenchmarking;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", descriptionKey = "iris.director.commanddeveloper.director.iris_world_manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
@Director(description = "Get Loaded TectonicPlates Count", descriptionKey = "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", origin = DirectorOrigin.BOTH, sync = true)
public void EngineStatus() {
Iris.service(IrisEngineSVC.class)
.engineStatus(sender());
}
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", descriptionKey = "iris.director.commanddeveloper.director.hash_generated_block_output_fixed_area_determinism_identity_testing", origin = DirectorOrigin.BOTH)
public void genhash(
@Param(description = "The world to hash", descriptionKey = "iris.director.commanddeveloper.param.world_hash", contextual = true, contextualOverride = true)
World world,
@Param(description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center", defaultValue = "4")
int radius,
@Param(description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x", defaultValue = "0")
int centerX,
@Param(description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z", defaultValue = "0")
int centerZ) {
if (world == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_WORLD_IS_NULL));
return;
}
VolmitSender sender = sender();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_STARTED_CHUNKS, MessageArgument.untrusted("value", ((radius * 2 + 1) * (radius * 2 + 1)))));
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
}
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
long startMs = M.ms();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
long globalHash = 0L;
long solidBlocks = 0L;
Map<String, Long> histogram = new TreeMap<>();
JsonObject chunkHashes = new JsonObject();
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
org.bukkit.ChunkSnapshot snapshot;
try {
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
snapshot = loaded.getChunkSnapshot(false, false, false);
} catch (Throwable e) {
Iris.reportError(e);
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_FAILED_AT_CHUNK, MessageArgument.untrusted("rx", rx), MessageArgument.untrusted("rz", rz), MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
return;
}
long chunkHash = 0L;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int worldX = (rx << 4) + x;
int worldZ = (rz << 4) + z;
for (int y = minY; y < maxY; y++) {
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
^ ((long) worldZ * 0x165667B19E3779F9L);
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
chunkHash ^= blockHash;
if (material != org.bukkit.Material.AIR
&& material != org.bukkit.Material.CAVE_AIR
&& material != org.bukkit.Material.VOID_AIR) {
histogram.merge(material.name(), 1L, Long::sum);
solidBlocks++;
}
}
}
}
globalHash ^= chunkHash;
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
}
}
int side = radius * 2 + 1;
JsonObject result = new JsonObject();
result.addProperty("global", Long.toHexString(globalHash));
result.addProperty("chunks", side * side);
result.addProperty("solidBlocks", solidBlocks);
result.addProperty("minY", minY);
result.addProperty("maxY", maxY);
JsonObject hist = new JsonObject();
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
hist.addProperty(entry.getKey(), entry.getValue());
}
result.add("histogram", hist);
result.add("chunkHashes", chunkHashes);
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
try {
Files.writeString(out.toPath(), result.toString());
} catch (IOException e) {
Iris.reportError(e);
}
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_GLOBAL_CHUNKS_SOLID, MessageArgument.untrusted("value", Long.toHexString(globalHash)), MessageArgument.untrusted("value2", (side * side)), MessageArgument.untrusted("solidBlocks", solidBlocks), MessageArgument.untrusted("value3", Form.duration((long) (M.ms() - startMs), 1))));
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
}
private static long genHashMix(long z) {
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world,
@Param(description = "The pack to install into the world", descriptionKey = "iris.director.commanddeveloper.param.pack_install_into_world", contextual = true, contextualOverride = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Make sure to make a backup & read the warnings first!", descriptionKey = "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", defaultValue = "false", aliases = "c")
boolean confirm
) {
if (!confirm) {
sender().sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
MessageArgument.untrusted("world", world.getName()),
MessageArgument.untrusted("pack", pack.getLoadKey())
));
return;
}
File folder = world.getWorldFolder();
folder.mkdirs();
try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
}
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test")
public void mantle(
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", descriptionKey = "iris.director.commanddeveloper.param.dump_whole_tectonic_plate_instead_single_section", defaultValue = "false")
boolean plate,
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", descriptionKey = "iris.director.commanddeveloper.param.dump_file_id_under_plugins_iris_dump_pv_id", defaultValue = "21474836474")
String name
) throws Throwable {
Engine activeEngine = engine();
if (activeEngine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_IRIS_WORLD_BEFORE_READING_MANTLE_DUMP));
return;
}
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
if (plate) {
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
} catch (Throwable e) {
Iris.reportError("Failed to inspect the Iris tectonic plate.", e);
}
} else {
Matter.read(section);
}
}
if (!TectonicPlate.hasError()) {
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
}
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_2")
public void packBenchmark(
@Param(description = "The pack to bench", descriptionKey = "iris.director.commanddeveloper.param.pack_bench", aliases = {"pack"}, defaultValue = "overworld")
IrisDimension dimension,
@Param(description = "Radius in regions", descriptionKey = "iris.director.commanddeveloper.param.radius_regions", defaultValue = "2048")
int radius,
@Param(description = "Open GUI while benchmarking", descriptionKey = "iris.director.commanddeveloper.param.open_gui_while_benchmarking", defaultValue = "false")
boolean gui
) {
new IrisPackBenchmarking(dimension, radius, gui);
}
@Director(description = "Upgrade to another Minecraft version", descriptionKey = "iris.director.commanddeveloper.director.upgrade_another_minecraft_version")
public void upgrade(
@Param(description = "The version to upgrade to", descriptionKey = "iris.director.commanddeveloper.param.version_upgrade", defaultValue = "latest") DataVersion version) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_UPGRADING, MessageArgument.untrusted("value", version.getVersion())));
ServerConfigurator.installDataPacks(version.get(), false);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DONE_UPGRADING_YOU_CAN_NOW_UPDATE_YOUR_SERVER_VERSION, MessageArgument.untrusted("value", version.getVersion())));
}
@Director(description = "test", descriptionKey = "iris.director.commanddeveloper.director.test_3")
public void mca (
@Param(description = "The world folder to scan for .mca region files", descriptionKey = "iris.director.commanddeveloper.param.world_folder_scan_mca_region_files") String world) {
try {
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
for (File mca : McaFiles) {
MCAFile MCARegion = MCAUtil.read(mca);
}
} catch (Exception e) {
Iris.reportError("Failed to inspect Minecraft region files.", e);
}
}
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0")
int radius
) {
if (radius < 0) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER));
return;
}
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
return;
}
Engine engine = access.getEngine();
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
// The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + ".");
}
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"})
public void network() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
Iris.info("Display Name: %s", ni.getDisplayName());
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
for (InetAddress ia : Collections.list(inetAddresses)) {
Iris.info("IP: %s", ia.getHostAddress());
}
}
} catch (Exception e) {
Iris.reportError("Failed to inspect server network interfaces.", e);
}
}
// --- Regen ---
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5")
int radius
) {
if (radius < 0) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_2));
return;
}
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
if (engine == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
return;
}
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
// The player position must be read on the thread owning the player; the regenerator hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule regen on the thread owning " + player.getName() + ".");
}
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
public void goldenhash(
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true, contextualOverride = true)
World world,
@Param(name = "radius", description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center_2", defaultValue = "8")
int radius,
@Param(name = "center-x", description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x_2", defaultValue = "0")
int centerX,
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
int centerZ,
@Param(name = "reset-mantle", description = "Delete the world's entire mantle folder first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
boolean resetMantle,
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8")
int threads,
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", descriptionKey = "iris.director.commanddeveloper.param.also_dump_full_per_chunk_non_air_blockstates_offline_diffing", defaultValue = "false")
boolean deep
) {
if (radius < 0) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_3));
return;
}
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_MUST_BE_IRIS_WORLD));
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_2));
return;
}
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_GOLDENHASH_STARTED_CHUNK_S_AROUND_BUFFERS_WORLD_UNTOUCHED, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("goldenhash start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
}
}
@@ -0,0 +1,122 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something", descriptionKey = "iris.director.commandedit.director.edit_something")
public class CommandEdit implements DirectorExecutor {
private boolean noStudio() {
if (!sender().isPlayer()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_PLAYERS_ONLY));
return true;
}
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_NO_STUDIO_WORLD_IS_OPEN));
return true;
}
if (!engine().isStudio()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_YOU_MUST_BE_STUDIO_WORLD));
return true;
}
if (GraphicsEnvironment.isHeadless()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_OPEN_FILES_HEADLESS_ENVIRONMENTS));
return true;
}
if (!Desktop.isDesktopSupported()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_DESKTOP_IS_NOT_SUPPORTED_BY_THIS_ENVIRONMENT));
return true;
}
return false;
}
@Director(description = "Edit the biome you specified", descriptionKey = "iris.director.commandedit.director.edit_biome_you_specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
public void biome(@Param(contextual = false, description = "The biome to edit", descriptionKey = "iris.director.commandedit.param.biome_edit") IrisBiome biome) {
if (noStudio()) {
return;
}
try {
if (biome == null || biome.getLoadFile() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM));
return;
}
Desktop.getDesktop().open(biome.getLoadFile());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE, MessageArgument.untrusted("value", biome.getTypeName()), MessageArgument.untrusted("value2", biome.getLoadFile().getName().split("\\Q.\\E")[0])));
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST));
}
}
@Director(description = "Edit the region you specified", descriptionKey = "iris.director.commandedit.director.edit_region_you_specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
public void region(@Param(contextual = false, description = "The region to edit", descriptionKey = "iris.director.commandedit.param.region_edit") IrisRegion region) {
if (noStudio()) {
return;
}
try {
if (region == null || region.getLoadFile() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_2));
return;
}
Desktop.getDesktop().open(region.getLoadFile());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_2, MessageArgument.untrusted("value", region.getTypeName()), MessageArgument.untrusted("value2", region.getLoadFile().getName().split("\\Q.\\E")[0])));
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_2));
}
}
@Director(description = "Edit the dimension you specified", descriptionKey = "iris.director.commandedit.director.edit_dimension_you_specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
public void dimension(@Param(contextual = false, description = "The dimension to edit", descriptionKey = "iris.director.commandedit.param.dimension_edit") IrisDimension dimension) {
if (noStudio()) {
return;
}
try {
if (dimension == null || dimension.getLoadFile() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_3));
return;
}
Desktop.getDesktop().open(dimension.getLoadFile());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_3, MessageArgument.untrusted("value", dimension.getTypeName()), MessageArgument.untrusted("value2", dimension.getLoadFile().getName().split("\\Q.\\E")[0])));
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_3));
}
}
}
@@ -0,0 +1,738 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.MessageArgument;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
public void biome(
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look")
IrisBiome biome,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD));
return;
}
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
}
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void region(
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
IrisRegion region,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_2));
return;
}
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
}
@Director(description = "Find a point of interest.", descriptionKey = "iris.director.commandfind.director.find_point_interest")
public void poi(
@Param(description = "The type of PoI to look for.", descriptionKey = "iris.director.commandfind.param.type_poi_look")
String type,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_3", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_3));
return;
}
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
String structure
) {
VolmitSender commandSender = sender();
if (commandSender == null) {
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
return;
}
Engine e = engine();
if (e == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD));
return;
}
Player target = player();
if (target == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE));
return;
}
String structureKey = structure == null ? "" : structure.trim();
Structure nativeStructure = resolveNativeStructure(structureKey);
boolean registered = nativeStructure != null;
IrisNativeStructureDecision decision = registered
? NativeStructureGenerationPolicy.resolve(e, structureKey, false)
: null;
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(e, structureKey);
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(e, structureKey);
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey);
World targetWorld = target.getWorld();
boolean nativeGenerationEnabled = targetWorld.canGenerateStructures();
boolean requiresReachability = registered && decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS
&& nativeGenerationEnabled;
boolean reachable = !requiresReachability || StructureReachability.isReachable(e, structureKey);
StructureLookupRoute route = selectStructureLookupRoute(
registered, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled, reachable);
if (route == StructureLookupRoute.UNKNOWN) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
return;
}
if (route == StructureLookupRoute.POLICY_DISABLED) {
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
structureKey, decision.status()));
return;
}
if (route == StructureLookupRoute.NO_ACTIVE_PLACEMENT) {
commandSender.sendMessage(C.YELLOW + structureKey
+ " has no active placement in this world.");
return;
}
if (route == StructureLookupRoute.WORLD_DISABLED) {
commandSender.sendMessage(C.YELLOW + structureKey
+ " cannot generate because native structure generation is disabled for this world.");
return;
}
if (route == StructureLookupRoute.UNREACHABLE) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
commandSender.sendMessage(C.YELLOW + structureKey
+ " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
if (route == StructureLookupRoute.IRIS) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
Location origin = target.getLocation();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING, MessageArgument.untrusted("structureKey", structureKey)));
J.s(() -> {
try {
StructureSearchResult result = targetWorld.locateNearestStructure(
origin, nativeStructure, 100, false);
if (result == null || result.getLocation() == null) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structureKey + " found within range of you.");
return;
}
prepareStructureTeleport(
target, targetWorld, commandSender, structureKey, result.getLocation(), false);
} catch (Throwable t) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structureKey + "'.", t);
}
});
}
@Director(description = "Print every structure excluded from /iris goto and its rejection reason to the server console", sync = true)
public void unregistered() {
VolmitSender commandSender = sender();
if (commandSender == null) {
Iris.reportError("Structure exclusion report started without a command sender context.",
new IllegalStateException("Missing command sender context"));
return;
}
Engine activeEngine = engine();
if (activeEngine == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD));
return;
}
Player target = player();
if (target == null) {
commandSender.sendMessage(C.RED + "Run this command from the Iris world to inspect its structures.");
return;
}
StructureExclusionReport snapshot;
try {
snapshot = collectStructureExclusions(
activeEngine, target.getWorld().canGenerateStructures());
} catch (Throwable error) {
commandSender.sendMessage(C.RED + "Could not build the structure exclusion report; see the server console.");
Iris.reportError("Could not snapshot /iris goto unregistered report for world '"
+ target.getWorld().getName() + "'.", error);
return;
}
String worldName = target.getWorld().getName();
J.a(() -> {
try {
StructureExclusionReport report = includeManagedDatapackStructures(
snapshot, DatapackIngestService.installed());
printStructureExclusionReport(worldName, report);
sendStructureMessage(target, commandSender, C.GREEN + "Printed " + report.entries().size()
+ " non-generating structure candidate(s) and their reasons to the server console. "
+ "This was an eligibility check; no chunks were searched.");
} catch (Throwable error) {
sendStructureMessage(target, commandSender,
C.RED + "Could not finish the structure exclusion report; see the server console.");
Iris.reportError("Could not finish /iris goto unregistered report for world '"
+ worldName + "'.", error);
}
});
}
private static StructureExclusionReport collectStructureExclusions(
Engine engine, boolean nativeGenerationEnabled) {
PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks();
Map<String, String> registeredKeys = distinctStructureKeys(structureHooks.structureKeys());
Set<String> reachableKeys = StructureReachability.reachableKeys(engine);
Set<String> possibleBiomeKeys = normalizedStructureKeys(
structureHooks.possibleBiomeKeys(engine.getWorld().platformWorld()));
List<StructureExclusion> exclusions = new ArrayList<>();
int registeredExcluded = 0;
for (Map.Entry<String, String> entry : registeredKeys.entrySet()) {
String normalizedKey = entry.getKey();
String key = entry.getValue();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(engine, key);
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
StructureLookupRoute route = selectStructureLookupRoute(
true, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled,
reachableKeys.contains(normalizedKey));
if (route == StructureLookupRoute.IRIS || route == StructureLookupRoute.NATIVE) {
continue;
}
List<String> requiredBiomes = needsNativeReachabilityReason(route, decision)
? structureHooks.structureBiomeKeys(key)
: List.of();
exclusions.add(new StructureExclusion(
StructureExclusionKind.EXCLUDED, key, null,
describeStructureExclusion(
route, key, decision.status(), nativePlacement,
requiredBiomes, possibleBiomeKeys)));
registeredExcluded++;
}
int configuredUnregistered = 0;
Set<String> configuredUnregisteredKeys = new LinkedHashSet<>();
for (String configuredKey : IrisStructureLocator.placedKeys(engine)) {
String normalizedKey = normalizeStructureKey(configuredKey);
if (normalizedKey.isEmpty()
|| registeredKeys.containsKey(normalizedKey)
|| !IrisStructureLocator.hasNativePlacement(engine, configuredKey)
|| !configuredUnregisteredKeys.add(normalizedKey)) {
continue;
}
exclusions.add(new StructureExclusion(
StructureExclusionKind.UNREGISTERED, configuredKey, null,
describeConfiguredUnregisteredNative(
IrisStructureLocator.hasLocatableNativePlacement(engine, configuredKey))));
configuredUnregistered++;
}
int editableUnplaced = 0;
Set<String> unplacedEditableKeys = new LinkedHashSet<>();
Set<String> locatableEditableKeys = normalizedStructureKeys(
IrisStructureLocator.locatableEditableKeys(engine));
for (String editableKey : engine.getData().getStructureLoader().getPossibleKeys()) {
String normalizedKey = normalizeStructureKey(editableKey);
if (!isUnplacedEditableCandidate(
normalizedKey, registeredKeys.keySet(), locatableEditableKeys,
IrisStructureLocator.hasNativePlacement(engine, editableKey))
|| !unplacedEditableKeys.add(normalizedKey)) {
continue;
}
boolean configuredPlacement = IrisStructureLocator.hasEditablePlacement(engine, editableKey);
String reason = configuredPlacement
? "editable Iris structure has a configured placement, but no matching placement is active "
+ "because its density is not positive or its Y band does not intersect this world"
: "editable Iris structure exists in this pack, but no structure placement references it";
exclusions.add(new StructureExclusion(
StructureExclusionKind.UNPLACED, editableKey, null, reason));
editableUnplaced++;
}
Set<String> configuredImportUrls = normalizedImportUrls(
engine.getDimension().getDatapackImports());
return new StructureExclusionReport(
List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()),
configuredImportUrls, registeredExcluded, configuredUnregistered,
editableUnplaced, 0);
}
static boolean isUnplacedEditableCandidate(
String normalizedKey, Set<String> registeredKeys,
Set<String> locatableEditableKeys, boolean nativePlacement) {
return normalizedKey != null
&& !normalizedKey.isEmpty()
&& !registeredKeys.contains(normalizedKey)
&& !locatableEditableKeys.contains(normalizedKey)
&& !nativePlacement;
}
private static StructureExclusionReport includeManagedDatapackStructures(
StructureExclusionReport snapshot, List<DatapackIngestService.Entry> installedEntries) {
Map<String, ManagedDatapackStructure> unregisteredStructures = new LinkedHashMap<>();
for (DatapackIngestService.Entry entry : installedEntries) {
String importUrl = normalizeImportUrl(entry.url);
if (importUrl.isEmpty() || !snapshot.configuredImportUrls().contains(importUrl)) {
continue;
}
String sourceId = entry.id == null || entry.id.isBlank() ? "unknown" : entry.id.trim();
List<String> structureKeys = entry.structureKeys == null ? List.of() : entry.structureKeys;
for (String key : structureKeys) {
String normalizedKey = normalizeStructureKey(key);
if (normalizedKey.isEmpty() || snapshot.registeredKeys().contains(normalizedKey)) {
continue;
}
ManagedDatapackStructure managed = unregisteredStructures.computeIfAbsent(
normalizedKey, ignored -> new ManagedDatapackStructure(key.trim()));
managed.sourceIds().add(sourceId);
}
}
List<StructureExclusion> entries = new ArrayList<>();
int configuredUnregistered = snapshot.configuredUnregistered();
for (StructureExclusion exclusion : snapshot.entries()) {
boolean replacedByManagedSource = exclusion.kind() == StructureExclusionKind.UNREGISTERED
&& unregisteredStructures.containsKey(normalizeStructureKey(exclusion.key()));
if (replacedByManagedSource) {
configuredUnregistered--;
continue;
}
entries.add(exclusion);
}
for (ManagedDatapackStructure managed : unregisteredStructures.values()) {
entries.add(new StructureExclusion(
StructureExclusionKind.UNREGISTERED, managed.key(),
String.join(",", managed.sourceIds()),
"declared by an Iris-managed datapack but absent from the live registry; "
+ "restart, enablement, or datapack validation may be required"));
}
sortStructureExclusions(entries);
return new StructureExclusionReport(
List.copyOf(entries), snapshot.registeredKeys(), snapshot.configuredImportUrls(),
snapshot.registeredExcluded(), Math.max(0, configuredUnregistered),
snapshot.editableUnplaced(), unregisteredStructures.size());
}
private static void printStructureExclusionReport(
String worldName, StructureExclusionReport report) {
Iris.info("Iris goto unregistered report for world '%s': %d non-generating structure candidate(s).",
worldName, report.entries().size());
for (StructureExclusion exclusion : report.entries()) {
String source = exclusion.source() == null
? ""
: " (source " + exclusion.source() + ")";
Iris.info("[%s] %s%s: %s",
exclusion.kind().label(), exclusion.key(), source, exclusion.reason());
}
Iris.info("Iris goto unregistered summary: %d registered key(s) excluded, "
+ "%d managed datapack key(s) unregistered, %d configured native key(s) unregistered, "
+ "%d editable Iris structure(s) unplaced.",
report.registeredExcluded(), report.managedUnregistered(),
report.configuredUnregistered(), report.editableUnplaced());
Iris.info("Eligibility report only; no chunks were searched and existing generated starts were not scanned.");
}
private static boolean needsNativeReachabilityReason(
StructureLookupRoute route, IrisNativeStructureDecision decision) {
return route == StructureLookupRoute.UNREACHABLE
|| route == StructureLookupRoute.NO_ACTIVE_PLACEMENT && decision.generate();
}
static String describeStructureExclusion(
StructureLookupRoute route, String key, NativeStructureGenerationStatus status,
boolean nativePlacement, List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
return switch (route) {
case UNKNOWN -> "the key is not registered by the active server/datapack";
case POLICY_DISABLED -> NativeStructureGenerationPolicy.generationStatusMessage(key, status);
case WORLD_DISABLED -> "native structure generation is disabled for this world";
case UNREACHABLE -> nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
case NO_ACTIVE_PLACEMENT -> {
String placementType = nativePlacement
? "configured nativeStructures placement"
: "configured Iris replacement";
String reason = placementType + " is inactive: no matching placement has positive density "
+ "when density-based and a Y band intersecting this world";
if (status == NativeStructureGenerationStatus.GENERATE_NATIVE) {
reason += "; the native route is also inactive because "
+ nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
}
yield reason;
}
case IRIS, NATIVE -> throw new IllegalArgumentException(
"Active structure route cannot be described as excluded: " + route);
};
}
static String nativeReachabilityReason(
List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
if (requiredBiomes == null || requiredBiomes.isEmpty()) {
return "its resolved biome filter is empty";
}
Set<String> possible = possibleBiomeKeys == null ? Set.of() : possibleBiomeKeys;
for (String requiredBiome : requiredBiomes) {
if (possible.contains(normalizeStructureKey(requiredBiome))) {
return "no active positive-weight, positive-frequency structure-set entry includes it in this world";
}
}
return "this pack does not produce any of its required biome(s): "
+ String.join("/", requiredBiomes);
}
static String describeConfiguredUnregisteredNative(boolean locatablePlacement) {
String reason = "configured in nativeStructures, but the key is not registered by the active "
+ "server/datapack, so Minecraft cannot create its native structure start";
return locatablePlacement
? reason
: reason + "; its Iris placement is also inactive because no matching placement has positive "
+ "density when density-based and a Y band intersecting this world";
}
private static Map<String, String> distinctStructureKeys(List<String> keys) {
Map<String, String> distinctKeys = new LinkedHashMap<>();
for (String key : keys) {
String normalizedKey = normalizeStructureKey(key);
if (!normalizedKey.isEmpty()) {
distinctKeys.putIfAbsent(normalizedKey, key.trim());
}
}
return distinctKeys;
}
private static Set<String> normalizedStructureKeys(Iterable<String> keys) {
Set<String> normalizedKeys = new LinkedHashSet<>();
if (keys == null) {
return normalizedKeys;
}
for (String key : keys) {
String normalizedKey = normalizeStructureKey(key);
if (!normalizedKey.isEmpty()) {
normalizedKeys.add(normalizedKey);
}
}
return normalizedKeys;
}
private static Set<String> normalizedImportUrls(Iterable<String> urls) {
Set<String> normalizedUrls = new LinkedHashSet<>();
if (urls == null) {
return normalizedUrls;
}
for (String url : urls) {
String normalizedUrl = normalizeImportUrl(url);
if (!normalizedUrl.isEmpty()) {
normalizedUrls.add(normalizedUrl);
}
}
return Set.copyOf(normalizedUrls);
}
private static String normalizeImportUrl(String url) {
return url == null ? "" : url.trim();
}
private static void sortStructureExclusions(List<StructureExclusion> exclusions) {
exclusions.sort((left, right) -> {
int kindComparison = left.kind().compareTo(right.kind());
return kindComparison == 0
? left.key().compareToIgnoreCase(right.key())
: kindComparison;
});
}
private static String normalizeStructureKey(String key) {
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
}
static StructureLookupRoute selectStructureLookupRoute(
boolean registered, IrisNativeStructureDecision decision, boolean nativePlacement,
boolean locatableNativePlacement, boolean locatableEditablePlacement,
boolean nativeGenerationEnabled, boolean reachable) {
if (!registered) {
return !nativePlacement && locatableEditablePlacement
? StructureLookupRoute.IRIS
: StructureLookupRoute.UNKNOWN;
}
if (decision == null) {
throw new IllegalArgumentException("Registered structure lookup requires a generation decision");
}
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
return StructureLookupRoute.POLICY_DISABLED;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
if (!nativePlacement) {
return locatableEditablePlacement
? StructureLookupRoute.IRIS
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
}
if (!nativeGenerationEnabled) {
return StructureLookupRoute.WORLD_DISABLED;
}
return locatableNativePlacement
? StructureLookupRoute.NATIVE
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
}
if (!nativeGenerationEnabled) {
return StructureLookupRoute.WORLD_DISABLED;
}
if (nativePlacement && locatableNativePlacement) {
return StructureLookupRoute.NATIVE;
}
if (reachable) {
return StructureLookupRoute.NATIVE;
}
return nativePlacement
? StructureLookupRoute.NO_ACTIVE_PLACEMENT
: StructureLookupRoute.UNREACHABLE;
}
private static Structure resolveNativeStructure(String structureKey) {
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
if (structureRegistry == null) {
return null;
}
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structureKey)) {
return candidate;
}
}
return null;
}
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
Player target = player();
if (target == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE_2));
return;
}
World targetWorld = target.getWorld();
Location origin = target.getLocation();
int blockX = origin.getBlockX();
int blockZ = origin.getBlockZ();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING_2, MessageArgument.untrusted("structure", structure)));
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
return;
}
if (!result.found()) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
return;
}
Location destination = new Location(
targetWorld, result.originX(), result.baseY(), result.originZ());
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
} catch (Throwable t) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
}
});
}
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
public void object(
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
String object,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_4));
return;
}
Player studioPlayer = player();
if (studioPlayer != null) {
try {
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_OBJECT_STUDIO_TELEPORTING, MessageArgument.untrusted("object", object)));
return;
}
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (e.hasObjectPlacement(object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS, MessageArgument.untrusted("object", object)));
}
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
}
});
}
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
try {
int y = useLocatedY
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
J.runEntity(target, () -> {
BukkitPlatform.teleportAsync(target, destination);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", at.getBlockX()), MessageArgument.untrusted("y", y), MessageArgument.untrusted("value2", at.getBlockZ())));
});
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
}
}
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
J.runEntity(target, () -> commandSender.sendMessage(message));
}
enum StructureLookupRoute {
UNKNOWN,
POLICY_DISABLED,
NO_ACTIVE_PLACEMENT,
WORLD_DISABLED,
UNREACHABLE,
IRIS,
NATIVE
}
private enum StructureExclusionKind {
UNREGISTERED("unregistered"),
EXCLUDED("excluded"),
UNPLACED("unplaced");
private final String label;
StructureExclusionKind(String label) {
this.label = label;
}
private String label() {
return label;
}
}
private record StructureExclusion(
StructureExclusionKind kind, String key, String source, String reason) {
}
private record StructureExclusionReport(
List<StructureExclusion> entries, Set<String> registeredKeys,
Set<String> configuredImportUrls, int registeredExcluded,
int configuredUnregistered, int editableUnplaced, int managedUnregistered) {
}
private record ManagedDatapackStructure(String key, Set<String> sourceIds) {
private ManagedDatapackStructure(String key) {
this(key, new LinkedHashSet<>());
}
}
}
@@ -0,0 +1,890 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.PendingWorldReplacementManager;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.IrisWorldRemovalService;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.File;
import java.nio.file.Files;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
public class CommandIris implements DirectorExecutor {
private static final String NO_DOWNLOAD_SOURCE = "__none__";
private static final String PRESERVE_REPLACEMENT_SEED = "preserve";
private static final long WORLD_UNLOAD_TIMEOUT_SECONDS = 150L;
private CommandStudio studio;
private CommandPregen pregen;
private CommandObject object;
private CommandStructure structure;
private CommandJigsaw jigsaw;
private CommandWhat what;
private CommandEdit edit;
private CommandDeveloper developer;
private CommandPack pack;
private CommandFind find;
private CommandDatapack datapack;
VolmitSender sender = Iris.getSender();
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create", descriptionKey = "iris.director.commandiris.param.name_world_create")
String name,
@Param(
aliases = {"dimension", "pack"},
description = "The dimension/pack to create the world with", descriptionKey = "iris.director.commandiris.param.dimension_pack_create_world_with",
defaultValue = "default",
customHandler = PackDimensionTypeHandler.class
)
String type,
@Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337")
long seed
) {
NamespacedKey worldKey;
try {
worldKey = IrisWorldStorage.managedKeyFromName(name);
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
} catch (IllegalArgumentException e) {
sender().sendMessage(C.RED + e.getMessage());
return;
}
String worldName = IrisWorldStorage.logicalName(worldKey);
if (worldName.equalsIgnoreCase("iris")) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD));
return;
}
if (worldName.equalsIgnoreCase("benchmark")) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_BENCHMARK_CREATING_WORLDS_AS_IRIS));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2));
return;
}
if (IrisWorldStorage.dimensionRoot(worldName).exists()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
return;
}
Optional<String> startupDenial = ServerConfigurator.worldCreationDenialReason(false);
if (startupDenial.isPresent()) {
sender().sendMessage(C.YELLOW + startupDenial.get());
return;
}
String resolvedType = type.equalsIgnoreCase("default")
? IrisSettings.get().getGenerator().getDefaultWorldType()
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
MessageArgument.untrusted("dimension", resolvedType)
));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return;
}
try {
IrisToolbelt.createWorld()
.dimension(resolvedType)
.name(worldName)
.seed(seed)
.sender(sender())
.studio(false)
.create();
} catch (Throwable e) {
if (reportExpectedCreationInterruption(e)) {
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS));
Iris.reportError("Exception raised during world creation for \"" + worldName + "\".", e);
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD));
}
@Director(
description = "Replace an existing world with Iris generation on the next restart",
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
aliases = {"override", "overwrite"}
)
public void replace(
@Param(
name = "target",
aliases = "world-name",
description = "The exact existing world slot to replace",
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart"
)
String target,
@Param(
aliases = {"dimension", "pack"},
description = "The dimension/pack to replace the world with",
defaultValue = "default",
customHandler = PackDimensionTypeHandler.class
)
String type,
@Param(
name = "seed",
aliases = "s",
description = "The replacement seed; omit to preserve the target world's seed",
defaultValue = PRESERVE_REPLACEMENT_SEED,
customHandler = ReplacementSeedHandler.class
)
Long seed
) {
NamespacedKey worldKey;
try {
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(target);
} catch (IllegalArgumentException e) {
sender().sendMessage(C.RED + e.getMessage());
return;
}
String resolvedType = type.equalsIgnoreCase("default")
? IrisSettings.get().getGenerator().getDefaultWorldType()
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
MessageArgument.untrusted("dimension", resolvedType)
));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return;
}
try {
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
.pendingWorldReplacements()
.stageReplacement(sender(), worldKey, dimension, seed);
String seedDetail = seed == null
? " preserving seed " + staged.seed()
: " using seed " + staged.seed();
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
+ "verifies the replacement.");
} catch (Throwable failure) {
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
? failure.getClass().getSimpleName()
: failure.getMessage();
sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail);
}
}
private boolean reportExpectedCreationInterruption(Throwable failure) {
Throwable current = failure;
while (current != null) {
if (current instanceof LifecycleOperationCoordinator.BusyException) {
sender().sendMessage(C.YELLOW + current.getMessage());
return true;
}
current = current.getCause();
}
String message = failure.getMessage();
if (message != null && message.contains("queued a restart")) {
sender().sendMessage(C.YELLOW + message);
return true;
}
return false;
}
@Director(description = "Teleport to another world", descriptionKey = "iris.director.commandiris.director.teleport_another_world", aliases = {"tp"}, sync = true)
public void teleport(
@Param(description = "World to teleport to", descriptionKey = "iris.director.commandiris.param.world_teleport")
World world,
@Param(description = "Player to teleport", descriptionKey = "iris.director.commandiris.param.player_teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
Player player
) {
if (player == null && sender().isPlayer())
player = sender().player();
final Player target = player;
if (target == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST));
return;
}
final Location spawn = world.getSpawnLocation();
final Runnable teleportTask = () -> {
BukkitPlatform.teleportAsync(target, spawn);
new VolmitSender(target).sendMessage(C.GREEN + IrisLanguage.text(
RuntimeUiMessages.TELEPORTED_TO_WORLD,
MessageArgument.untrusted("world", world.getName())
));
};
if (!J.runEntity(target, teleportTask)) {
teleportTask.run();
}
}
@Director(description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
public void version() {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
}
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TO, MessageArgument.untrusted("value", sender().player().getWorld().getMinHeight()), MessageArgument.untrusted("value2", sender().player().getWorld().getMaxHeight())));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TOTAL_HEIGHT, MessageArgument.untrusted("value", (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()))));
} else {
World mainWorld = getServer().getWorlds().get(0);
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
}
}
@Director(description = "Check access of all worlds.", descriptionKey = "iris.director.commandiris.director.check_access_all_worlds", aliases = {"accesslist"})
public void worlds() {
KList<World> IrisWorlds = new KList<>();
KList<World> BukkitWorlds = new KList<>();
for (World w : Bukkit.getServer().getWorlds()) {
try {
Engine engine = IrisToolbelt.access(w).getEngine();
if (engine != null) {
IrisWorlds.add(w);
}
} catch (Exception e) {
BukkitWorlds.add(w);
}
}
if (sender().isPlayer()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_WORLDS));
for (World IrisWorld : IrisWorlds.copy()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE, MessageArgument.untrusted("value", IrisWorld.getName())));
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_BUKKIT_WORLDS));
for (World BukkitWorld : BukkitWorlds.copy()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE_2, MessageArgument.untrusted("value", BukkitWorld.getName())));
}
} else {
Iris.info(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
Iris.info(C.IRIS + "- " +IrisWorld.getName());
}
Iris.info(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
}
}
}
@Director(description = "Remove an Iris world", descriptionKey = "iris.director.commandiris.director.remove_iris_world", aliases = {"rm"}, sync = true)
public void remove(
@Param(description = "The loaded or disk-only Iris world to remove", descriptionKey = "iris.director.commandiris.param.world_remove", customHandler = ManagedWorldNameHandler.class)
String world,
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", descriptionKey = "iris.director.commandiris.param.whether_also_remove_folder_if_set_false_just_does_not_load_world", defaultValue = "true")
boolean delete
) {
VolmitSender responseSender = sender();
responseSender.sendMessage(C.GRAY + "Removing Iris world '" + world + "'...");
IrisWorldRemovalService.get().remove(world, delete).whenComplete((result, throwable) -> {
Runnable response = () -> reportRemovalResult(responseSender, world, result, throwable);
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
return;
}
J.s(response);
});
}
private void reportRemovalResult(
VolmitSender responseSender,
String requestedWorld,
IrisWorldRemovalService.RemovalResult result,
Throwable throwable
) {
if (throwable != null || result == null) {
Throwable failure = throwable == null
? new IllegalStateException("World removal returned no result.")
: throwable;
responseSender.sendMessage(C.RED + "World removal failed unexpectedly; nothing further was deleted.");
Iris.reportError("Unexpected world removal failure for \"" + requestedWorld + "\".", failure);
return;
}
switch (result.status()) {
case UNREGISTERED -> responseSender.sendMessage(C.GREEN + "Unloaded and unregistered '"
+ result.target().logicalName() + "'; its files were preserved.");
case DELETED -> responseSender.sendMessage(C.GREEN + "Removed Iris world '"
+ result.target().logicalName() + "' and deleted its folder.");
case DELETE_QUEUED -> responseSender.sendMessage(C.YELLOW + "Removed Iris world '"
+ result.target().logicalName() + "'; its quarantined folder will be deleted at startup.");
case BUSY -> responseSender.sendMessage(C.YELLOW + "World changes are busy with "
+ result.blockingOperation().kind().name().toLowerCase(Locale.ROOT) + " for '"
+ result.blockingOperation().target() + "'. Try again when it completes.");
case INVALID_IDENTIFIER, PROTECTED_WORLD, NOT_IRIS_WORLD, UNSAFE_PATH, NOT_FOUND ->
responseSender.sendMessage(C.RED + removalFailureDetail(result));
default -> {
responseSender.sendMessage(C.RED + "World removal stopped at "
+ result.status().name().toLowerCase(Locale.ROOT) + ": " + removalFailureDetail(result));
if (result.quarantineDirectory() != null) {
responseSender.sendMessage(C.YELLOW + "The recoverable world folder is "
+ result.quarantineDirectory().toAbsolutePath() + ".");
} else if (result.configurationChanged() || result.registryChanged()) {
responseSender.sendMessage(C.YELLOW + "Removal changed registration state before stopping; "
+ "the original world folder was not deleted.");
}
if (result.failure() != null) {
Iris.reportError("World removal failed for \"" + requestedWorld + "\" at "
+ result.status().name() + ".", result.failure());
}
}
}
}
private String removalFailureDetail(IrisWorldRemovalService.RemovalResult result) {
Throwable failure = result.failure();
if (failure == null || failure.getMessage() == null || failure.getMessage().isBlank()) {
return result.status().name().toLowerCase(Locale.ROOT).replace('_', ' ');
}
return failure.getMessage();
}
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
public void debug() {
boolean to = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG, MessageArgument.untrusted("to", to)));
}
@Director(description = "Download a project.", descriptionKey = "iris.director.commandiris.director.download_project", aliases = "dl")
public void download(
@Param(name = "pack", description = "The built-in pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", defaultValue = NO_DOWNLOAD_SOURCE, customHandler = DownloadPackHandler.class)
String pack,
@Param(name = "link", description = "A direct HTTP or HTTPS zip link", defaultValue = NO_DOWNLOAD_SOURCE)
String link
) {
String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack;
String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link;
if ((builtInPack == null) == (directLink == null)) {
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_SOURCE));
return;
}
if (builtInPack != null) {
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack);
return;
}
if (!PackDownloader.isDirectZipUrl(directLink)) {
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL));
return;
}
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink);
}
@Director(description = "Get metrics for your world", descriptionKey = "iris.director.commandiris.director.get_metrics_your_world", aliases = "measure", origin = DirectorOrigin.PLAYER)
public void metrics() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD));
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SENDING_METRICS));
engine().printMetrics(sender());
}
@Director(description = "Reload configuration file (this is also done automatically)", descriptionKey = "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically")
public void reload() {
IrisSettings.invalidate();
IrisSettings.get();
boolean localeLoaded = IrisLanguage.reload();
if (localeLoaded) {
sender().sendMessage(C.GREEN + IrisLanguage.text(
IrisMessages.COMMAND_RELOAD_SUCCESS,
MessageArgument.trusted("locale", IrisLanguage.activeLocale())
));
return;
}
sender().sendMessage(C.YELLOW + IrisLanguage.text(
IrisMessages.COMMAND_RELOAD_FAILED,
MessageArgument.untrusted("locale", IrisSettings.get().getGeneral().getLanguage()),
MessageArgument.trusted("activeLocale", IrisLanguage.activeLocale())
));
}
@Director(description = "Unload an Iris World", descriptionKey = "iris.director.commandiris.director.unload_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
public void unloadWorld(
@Param(description = "The world to unload", descriptionKey = "iris.director.commandiris.param.world_unload")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_2, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()))));
return;
}
VolmitSender responseSender = sender();
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_UNLOADING_WORLD, MessageArgument.untrusted("value", world.getName())));
LifecycleOperationCoordinator.Lease lease;
try {
lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
LifecycleOperationCoordinator.OperationKind.WORLD_UNLOAD,
WorldIdentity.serialize(world)
);
} catch (LifecycleOperationCoordinator.BusyException e) {
responseSender.sendMessage(C.YELLOW + e.getMessage());
return;
}
PlatformChunkGenerator generator = IrisToolbelt.access(world);
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
try {
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
.thenCompose(evacuated -> {
if (terminalTimeout.get()) {
return CompletableFuture.failedFuture(new TimeoutException(
"World unload stopped after its terminal timeout."));
}
if (!Boolean.TRUE.equals(evacuated)) {
return CompletableFuture.completedFuture(false);
}
return WorldLifecycleService.get().unloadAsync(world, true);
})
.thenCompose(unloaded -> {
if (terminalTimeout.get()) {
return CompletableFuture.failedFuture(new TimeoutException(
"World unload stopped after its terminal timeout."));
}
if (!Boolean.TRUE.equals(unloaded) || generator == null) {
return CompletableFuture.completedFuture(Boolean.TRUE.equals(unloaded));
}
return generator.closeAsync().thenApply(ignored -> true);
});
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
.whenComplete((unloaded, throwable) -> {
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
lease.close();
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
return;
}
J.s(response);
});
} catch (Exception e) {
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
lease.close();
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
}
}
private CompletableFuture<Boolean> guardUnloadCompletion(
CompletableFuture<Boolean> source,
AtomicBoolean terminalTimeout,
String worldName
) {
CompletableFuture<Boolean> guarded = new CompletableFuture<>();
AtomicBoolean settled = new AtomicBoolean(false);
source.whenComplete((unloaded, throwable) -> {
if (!settled.compareAndSet(false, true)) {
return;
}
if (throwable == null) {
guarded.complete(Boolean.TRUE.equals(unloaded));
} else {
guarded.completeExceptionally(throwable);
}
});
CompletableFuture.delayedExecutor(WORLD_UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> {
if (!settled.compareAndSet(false, true)) {
return;
}
terminalTimeout.set(true);
TimeoutException timeout = new TimeoutException(
"World unload did not settle within " + WORLD_UNLOAD_TIMEOUT_SECONDS
+ " seconds for \"" + worldName + "\".");
ServerConfigurator.restart("World unload timed out for \"" + worldName + "\".");
guarded.completeExceptionally(timeout);
});
return guarded;
}
private void reportUnloadResult(VolmitSender responseSender, World world, Boolean unloaded, Throwable throwable) {
if (throwable != null) {
responseSender.sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3,
MessageArgument.untrusted("value", String.valueOf(throwable.getMessage()))
));
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", throwable);
return;
}
if (Boolean.TRUE.equals(unloaded)) {
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_UNLOADED_SUCCESSFULLY));
} else {
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_2));
}
}
@Director(description = "Load an Iris World", descriptionKey = "iris.director.commandiris.director.load_iris_world", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
public void loadWorld(
@Param(
description = "The name of the world to load",
descriptionKey = "iris.director.commandiris.param.name_world_load",
customHandler = ManagedWorldNameHandler.class)
String world
) {
NamespacedKey worldKey;
try {
worldKey = IrisWorldStorage.managedKeyFromName(world);
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
} catch (IllegalArgumentException failure) {
sender().sendMessage(C.RED + failure.getMessage());
return;
}
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
boolean worldExists = doesWorldExist(logicalWorldName);
if (!worldExists) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOESNT_EXIST_ON_SERVER, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
return;
}
VolmitSender responseSender = sender();
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADING_WORLD, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
Iris.instance.worldReconciler()
.loadWorld(BUKKIT_YML, worldKey.toString())
.whenComplete((result, failure) -> {
Runnable response = () -> reportLoadWorldResult(
responseSender,
logicalWorldName,
result,
failure);
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
return;
}
J.s(response);
});
}
private void reportLoadWorldResult(
VolmitSender responseSender,
String logicalWorldName,
BukkitWorldReconciler.LoadResult result,
Throwable failure
) {
if (failure != null) {
responseSender.sendMessage(C.RED + "Failed to load Iris world \"" + logicalWorldName + "\": " + failure.getMessage());
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", failure);
return;
}
if (result == null) {
IllegalStateException missingResult = new IllegalStateException("World load completed without a result.");
responseSender.sendMessage(C.RED + missingResult.getMessage());
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", missingResult);
return;
}
if (result.succeeded()) {
responseSender.sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY,
MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
return;
}
C color = result.status() == BukkitWorldReconciler.ReconciliationStatus.BUSY
|| result.status() == BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED
? C.YELLOW
: C.RED;
responseSender.sendMessage(color + result.message());
Throwable resultFailure = result.failure();
if (resultFailure != null
&& result.status() != BukkitWorldReconciler.ReconciliationStatus.BUSY
&& result.status() != BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED) {
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", resultFailure);
}
}
@Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
public void evacuate(
@Param(description = "Evacuate the world", descriptionKey = "iris.director.commandiris.param.evacuate_world")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()))));
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EVACUATING_WORLD, MessageArgument.untrusted("value", world.getName())));
IrisToolbelt.evacuate(world);
}
boolean doesWorldExist(String worldName) {
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName);
File levelRoot = IrisWorldStorage.levelRoot();
return IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()),
worldKey
).isPresent();
}
public static class ManagedWorldNameHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> options = new LinkedHashSet<>();
for (World world : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(world)) {
continue;
}
options.add(IrisWorldStorage.logicalName(world));
String startupName = configuredStartupName(world);
if (startupName != null) {
options.add(startupName);
}
}
for (String identity : IrisWorlds.get().getWorlds().keySet()) {
try {
options.add(IrisWorldStorage.logicalName(WorldIdentity.parse(identity)));
} catch (IllegalArgumentException ignored) {
}
}
File namespace = new File(IrisWorldStorage.levelRoot(), "dimensions/iris");
File[] diskWorlds = namespace.listFiles(File::isDirectory);
if (diskWorlds != null) {
for (File diskWorld : diskWorlds) {
if (!Files.isSymbolicLink(diskWorld.toPath())
&& diskWorld.getName().matches("[a-z0-9_-]+")) {
options.add(diskWorld.getName());
}
}
}
return new KList<>(options);
}
/**
* "iris worlds" prints the Bukkit startup name, so it has to be offered and accepted here too.
*/
private static String configuredStartupName(World world) {
try {
String configured = IrisWorldStorage.configuredWorldName(
WorldIdentity.key(world),
IrisWorldStorage.levelRoot().getName()
);
return configured.equals(world.getName()) ? configured : null;
} catch (RuntimeException failure) {
return null;
}
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.isBlank()) {
throw new DirectorParsingException("World identifier cannot be empty");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return packDimensionOptions(Iris.instance.getDataFolder("packs"));
}
static KList<String> packDimensionOptions(File packsFolder) {
Set<String> options = new LinkedHashSet<>();
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) {
options.add(pack.getName());
try {
IrisData data = IrisData.get(pack);
for (String key : data.getDimensionLoader().getPossibleKeys()) {
options.add(packDimensionOption(pack.getName(), key));
}
} catch (Throwable ex) {
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
pack.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
return new KList<>(options);
}
static String packDimensionOption(String packName, String dimensionKey) {
return packName.equalsIgnoreCase(dimensionKey)
? packName
: packName + ":" + dimensionKey;
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.trim().isEmpty()) {
throw new DirectorParsingException("World type cannot be empty");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
public static class ReplacementSeedHandler implements DirectorParameterHandler<Long> {
@Override
public KList<Long> getPossibilities() {
return null;
}
@Override
public String toString(Long value) {
return value == null ? PRESERVE_REPLACEMENT_SEED : Long.toString(value);
}
@Override
public Long parse(String in, boolean force) throws DirectorParsingException {
String seed = in == null ? "" : in.trim();
if (PRESERVE_REPLACEMENT_SEED.equalsIgnoreCase(seed)) {
return null;
}
try {
return Long.parseLong(seed);
} catch (NumberFormatException failure) {
throw new DirectorParsingException("Seed must be a signed 64-bit integer");
}
}
@Override
public boolean supports(Class<?> type) {
return type == Long.class;
}
@Override
public String getRandomDefault() {
return "1337";
}
}
public static class DownloadPackHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>(PackDownloader.builtInPacks());
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (NO_DOWNLOAD_SOURCE.equals(in)) {
return null;
}
String pack = in == null ? "" : in.trim().toLowerCase(Locale.ROOT);
if (!PackDownloader.isBuiltInPack(pack)) {
throw new DirectorParsingException("Pack must be 'overworld' or 'underworld'");
}
return pack;
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -0,0 +1,843 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation", descriptionKey = "iris.director.commandobject.director.iris_object_manipulation")
public class CommandObject implements DirectorExecutor {
static final Set<Material> PASTE_TRANSPARENT_BLOCKS = Set.of(
Material.AIR,
Material.CAVE_AIR,
Material.VOID_AIR,
Material.SHORT_GRASS,
Material.SNOW,
Material.VINE,
Material.TORCH,
Material.DEAD_BUSH,
Material.POPPY,
Material.DANDELION
);
static boolean isPasteTarget(Material material) {
return !PASTE_TRANSPARENT_BLOCKS.contains(material);
}
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", descriptionKey = "iris.director.commandobject.director.open_object_studio_world_grid_every_object_dimension_optional_defaults_all_packs", sync = true)
public void studio(
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", descriptionKey = "iris.director.commandobject.param.optional_dimension_whose_object_pack_lay_out_omit_aggregate_objects_from_every", aliases = "dim", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandobject.param.seed_generate_studio_with", aliases = "s")
long seed
) {
VolmitSender commandSender = sender();
Map<String, IrisData> sources = new LinkedHashMap<>();
IrisDimension hostDimension = dimension;
if (dimension != null) {
IrisData data = dimension.getLoader();
if (data == null) {
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
}
sources.put(data.getDataFolder().getName(), data);
} else {
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(workspace)) {
File dimensionsDir = new File(pack, "dimensions");
if (!dimensionsDir.isDirectory()) continue;
IrisData data = IrisData.get(pack);
String[] keys = data.getObjectLoader().getPossibleKeys();
if (keys == null || keys.length == 0) continue;
sources.put(pack.getName(), data);
if (hostDimension == null) {
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
if (dimFiles != null && dimFiles.length > 0) {
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
if (loaded != null) {
hostDimension = loaded;
}
}
}
}
}
if (hostDimension == null || sources.isEmpty()) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_NO_PACKS_WITH_OBJECTS_WERE_FOUND_ON_THIS_SERVER));
return;
}
int totalObjects = 0;
for (IrisData d : sources.values()) {
String[] k = d.getObjectLoader().getPossibleKeys();
if (k != null) totalObjects += k.length;
}
if (totalObjects == 0) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_NO_OBJECTS_PLACE_ACROSS_SELECTED_PACK_S));
return;
}
// ObjectStudioActivation carries the buffet state; mutating the shared cached
// IrisDimension here leaked OBJECT_BUFFET into later pack exports of this dimension.
ObjectStudioActivation.activate(hostDimension.getLoadKey());
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
String scope = dimension == null
? ("all packs [" + sources.size() + "]")
: ("\"" + hostDimension.getName() + "\"");
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_OPENING_OBJECT_STUDIO_OBJECTS, MessageArgument.untrusted("scope", scope), MessageArgument.untrusted("totalObjects", totalObjects)));
IrisDimension finalHost = hostDimension;
try {
Iris.service(StudioSVC.class).open(
commandSender,
seed,
hostDimension.getLoadKey(),
StudioOpenCoordinator.StudioOpenKind.OBJECT,
world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
} catch (Throwable e) {
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
}
}
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges, Engine targetEngine) {
return new IObjectPlacer() {
@Override
public int getHighest(int x, int z, IrisData data) {
return world.getHighestBlockYAt(x, z);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING);
}
@Override
public void set(int x, int y, int z, PlatformBlockState s) {
BlockData d = (BlockData) s.nativeHandle();
Block block = world.getBlockAt(x, y, z);
//Prevent blocks being set in or bellow bedrock
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
futureBlockChanges.putIfAbsent(block, block.getBlockData());
if (d instanceof IrisCustomData data) {
block.setBlockData(data.getBase(), false);
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
} else block.setBlockData(d, false);
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData());
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
return world.getBlockAt(x, y, z).getType().isSolid();
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public int getFluidHeight() {
return targetEngine == null
? 63
: targetEngine.getMinHeight() + targetEngine.getDimension().getFluidHeight();
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
tile.toBukkitTry(world.getBlockAt(xx, yy, zz));
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return null;
}
@Override
public Engine getEngine() {
return targetEngine;
}
};
}
@Director(description = "Check the composition of an object", descriptionKey = "iris.director.commandobject.director.check_composition_object")
public void analyze(
@Param(description = "The object to analyze", descriptionKey = "iris.director.commandobject.param.object_analyze", customHandler = ObjectHandler.class)
String object
) {
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_USED, MessageArgument.untrusted("value", NumberFormat.getIntegerInstance().format(o.getBlocks().size()))));
var queue = o.getBlocks().values();
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
Map<BlockData, Integer> amounts = new HashMap<>();
Map<Material, Integer> materials = new HashMap<>();
while (queue.hasNext()) {
BlockData block = (BlockData) queue.next().nativeHandle();
//unsorted.put(block.getMaterial(), block);
if (!amounts.containsKey(block)) {
amounts.put(block, 1);
} else
amounts.put(block, amounts.get(block) + 1);
if (!materials.containsKey(block.getMaterial())) {
materials.put(block.getMaterial(), 1);
unsorted.put(block.getMaterial(), new HashSet<>());
unsorted.get(block.getMaterial()).add(block);
} else {
materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1);
unsorted.get(block.getMaterial()).add(block);
}
}
List<Material> sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial)
.sorted().toList();
Set<Material> sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed());
sortedMats.addAll(sortedMatsList);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_OBJECT));
int n = 0;
for (Material mat : sortedMats) {
int amount = materials.get(mat);
List<BlockData> set = new ArrayList<>(unsorted.get(mat));
set.sort(Comparator.comparingInt(amounts::get).reversed());
BlockData data = set.get(0);
int dataAmount = amounts.get(data);
String string = " - " + mat.toString() + "*" + amount;
if (data.getAsString(true).contains("[")) {
string = string + " --> [" + data.getAsString(true).split("\\[")[1]
.replaceAll("true", ChatColor.GREEN + "true" + ChatColor.GRAY)
.replaceAll("false", ChatColor.RED + "false" + ChatColor.GRAY) + "*" + dataAmount;
}
sender().sendMessage(string);
n++;
if (n >= 10) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OTHER_BLOCK_TYPES, MessageArgument.untrusted("value", (sortedMats.size() - n))));
return;
}
}
}
@Director(description = "Shrink an object to its minimum size", descriptionKey = "iris.director.commandobject.director.shrink_object_its_minimum_size")
public void shrink(@Param(description = "The object to shrink", descriptionKey = "iris.director.commandobject.param.object_shrink", customHandler = ObjectHandler.class) String object) {
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_CURRENT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
o.shrinkwrap();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NEW_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
try {
o.write(o.getLoadFile());
} catch (IOException e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", o.getLoadFile()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
Iris.reportError("Failed to save object " + o.getLoadFile() + ".", e);
}
}
@Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay", descriptionKey = "iris.director.commandobject.director.grow_organic_branches_through_canopy_so_every_leaf_survives_vanilla_decay",
origin = DirectorOrigin.BOTH)
public void plausibilize(
@Param(description = "Object key, prefix (trees/), or filesystem path", descriptionKey = "iris.director.commandobject.param.object_key_prefix_trees_filesystem_path",
customHandler = ObjectTargetHandler.class)
String target,
@Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", descriptionKey = "iris.director.commandobject.param.dryrun_true_analyzes_only_writes_nothing", defaultValue = "false")
boolean dryRun,
@Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", descriptionKey = "iris.director.commandobject.param.reach_n_max_branch_length_blocks_from_existing_wood_farther_leaf_clusters", defaultValue = "12")
int reach
) {
IrisData nearest = data();
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, nearest);
if (targets.isEmpty() && nearest == null) {
targets = resolveFromPacks(target);
}
if (targets.isEmpty()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_OBJECTS_MATCHED, MessageArgument.untrusted("target", target)));
return;
}
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_OBJECT_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S,
MessageArgument.trusted("reach", reach),
MessageArgument.trusted("value", dryRun ? IrisLanguage.text(RuntimeUiMessages.TREE_DRY_SUFFIX) : ""),
MessageArgument.trusted("value2", targets.size())
));
VolmitSender s = sender();
List<TreePlausibilizeBatch.Target> queued = targets;
J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (TreePlausibilizeBatch.Output output) ->
s.sendMessage(output.headline()
? C.IRIS + output.text()
: C.GRAY + " " + output.text())));
}
private static List<TreePlausibilizeBatch.Target> resolveFromPacks(String target) {
List<TreePlausibilizeBatch.Target> out = new ArrayList<>();
File packsFolder = Iris.instance.getDataFolder("packs");
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) {
continue;
}
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out);
}
}
return out;
}
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.", descriptionKey = "iris.director.commandobject.director.convert_schem_files_convert_folder_iob_files")
public void convert () {
try {
IrisConverter.convertSchematics(sender());
} catch (Exception e) {
Iris.reportError("Failed to convert schematics to Iris objects.", e);
}
}
@Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d")
public void dust() {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createDust());
commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
});
}
@Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-")
public void contract(
@Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1")
int amount
) {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
return;
}
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1")
public void position1(
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true")
boolean here
) {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
return;
}
if (WandSVC.isHoldingWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[1] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2")
public void position2(
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true")
boolean here
) {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
return;
}
if (WandSVC.isHoldingIrisWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true)
public void paste(
@Param(description = "The object to paste", descriptionKey = "iris.director.commandobject.param.object_paste", customHandler = ObjectHandler.class)
String object,
@Param(description = "Whether or not to edit the object (need to hold wand)", descriptionKey = "iris.director.commandobject.param.whether_not_edit_object_need_hold_wand", defaultValue = "false")
boolean edit,
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
int rotate,
@Param(description = "The factor by which to scale the object placement", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "1")
double scale
// ,
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator
) {
IrisObject o = IrisData.loadAnyObject(object, data());
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
if (scale > maxScale) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale)));
scale = maxScale;
}
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
VolmitSender commandSender = sender();
Player player = player();
ItemStack wand = player.getInventory().getItemInMainHand();
Block targetBlock = player.getTargetBlock(PASTE_TRANSPARENT_BLOCKS, 256);
if (!isPasteTarget(targetBlock.getType())) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
return;
}
Location block = targetBlock.getLocation().clone().add(0, 1, 0);
commandSender.playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
Map<Block, BlockData> futureChanges = new HashMap<>();
if (scale != 1) {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
}
// Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o;
if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), null);
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
if (!edit) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
return;
}
onPlayerThread(player, () -> {
ObjectPasteBounds bounds = ObjectPasteBounds.resolve(placed, placement.getRotation(), block.getBlockX(),
block.getBlockY() + placed.getCenter().getBlockY(), block.getBlockZ());
Location minimum = new Location(block.getWorld(), bounds.minX(), bounds.minY(), bounds.minZ());
Location maximum = new Location(block.getWorld(), bounds.maxX(), bounds.maxY(), bounds.maxZ());
ItemStack newWand = WandSVC.createWand(maximum, minimum);
if (WandSVC.isWand(wand)) {
player.getInventory().setItemInMainHand(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
int slot = WandSVC.findWand(player.getInventory());
if (slot == -1) {
player.getInventory().addItem(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
player.getInventory().setItem(slot, newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
}
}
});
})) {
Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + ".");
}
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + ".");
}
}
@Director(description = "Save an object", descriptionKey = "iris.director.commandobject.director.save_object")
public void save(
@Param(description = "The dimension to store the object in", descriptionKey = "iris.director.commandobject.param.dimension_store_object", contextual = true, contextualOverride = true)
IrisDimension dimension,
@Param(description = "The file to store it in, can use / for subfolders", descriptionKey = "iris.director.commandobject.param.file_store_it_can_use_subfolders")
String name,
@Param(description = "Overwrite existing object files", descriptionKey = "iris.director.commandobject.param.overwrite_existing_object_files", defaultValue = "false", aliases = "force")
boolean overwrite,
@Param(description = "Use legacy TileState serialization if possible", descriptionKey = "iris.director.commandobject.param.use_legacy_tilestate_serialization_if_possible", defaultValue = "true")
boolean legacy
) {
IrisObject o = WandSVC.createSchematic(player(), legacy);
if (o == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_NEED_HOLD_YOUR_WAND));
return;
}
File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob");
if (file.exists() && !overwrite) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FILE_ALREADY_EXISTS_SET_OVERWRITE_TRUE_OVERWRITE_IT));
return;
}
try {
o.write(file, sender());
} catch (IOException e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
Iris.reportError(e);
return;
}
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_SUCCESSFULLY_OBJECT_SAVED_OBJECTS, MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("name", name)));
}
@Director(description = "Shift a selection in your looking direction", descriptionKey = "iris.director.commandobject.director.shift_selection_your_looking_direction")
public void shift(
@Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1")
int amount
) {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
return;
}
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
a1.add(d.toVector().multiply(amount));
a2.add(d.toVector().multiply(amount));
Cuboid cursor = new Cuboid(a1, a2);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u")
public void undo(
@Param(description = "The amount of pastes to undo", descriptionKey = "iris.director.commandobject.param.amount_pastes_undo", defaultValue = "1")
int amount
) {
ObjectSVC service = Iris.service(ObjectSVC.class);
int actualReverts = Math.min(service.getUndos().size(), amount);
service.revertChanges(actualReverts);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_REVERTED_PASTES, MessageArgument.untrusted("actualReverts", actualReverts)));
}
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER)
public void we() {
VolmitSender commandSender = sender();
Player player = player();
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
return;
}
Cuboid locs = WorldEditLink.getSelection(player);
if (locs == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
return;
}
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
});
}
@Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true)
public void wand() {
player().getInventory().addItem(WandSVC.createWand());
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_POOF_GOOD_LUCK_BUILDING));
}
@Director(name = "x&y", description = "Autoselect up, down & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_down_out", sync = true)
public void xay() {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_3));
return;
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_3));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Location a1x = b[0].clone();
Location a2x = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Cuboid cursorx = new Cuboid(a1, a2);
while (!cursor.containsOnly(Material.AIR)) {
a1.add(new org.bukkit.util.Vector(0, 1, 0));
a2.add(new org.bukkit.util.Vector(0, 1, 0));
cursor = new Cuboid(a1, a2);
}
a1.add(new org.bukkit.util.Vector(0, -1, 0));
a2.add(new org.bukkit.util.Vector(0, -1, 0));
while (!cursorx.containsOnly(Material.AIR)) {
a1x.add(new org.bukkit.util.Vector(0, -1, 0));
a2x.add(new org.bukkit.util.Vector(0, -1, 0));
cursorx = new Cuboid(a1x, a2x);
}
a1x.add(new org.bukkit.util.Vector(0, 1, 0));
a2x.add(new Vector(0, 1, 0));
b[0] = a1;
b[1] = a2x;
cursor = new Cuboid(b[0], b[1]);
cursor = cursor.contract(Cuboid.CuboidDirection.North);
cursor = cursor.contract(Cuboid.CuboidDirection.South);
cursor = cursor.contract(Cuboid.CuboidDirection.East);
cursor = cursor.contract(Cuboid.CuboidDirection.West);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE));
}
@Director(name = "x+y", description = "Autoselect up & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_out", sync = true)
public void xpy() {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_4));
return;
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_4));
return;
}
b[0].add(new Vector(0, 1, 0));
b[1].add(new Vector(0, 1, 0));
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
while (!cursor.containsOnly(Material.AIR)) {
a1.add(new Vector(0, 1, 0));
a2.add(new Vector(0, 1, 0));
cursor = new Cuboid(a1, a2);
}
a1.add(new Vector(0, -1, 0));
a2.add(new Vector(0, -1, 0));
b[0] = a1;
a2 = b[1];
cursor = new Cuboid(a1, a2);
cursor = cursor.contract(Cuboid.CuboidDirection.North);
cursor = cursor.contract(Cuboid.CuboidDirection.South);
cursor = cursor.contract(Cuboid.CuboidDirection.East);
cursor = cursor.contract(Cuboid.CuboidDirection.West);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2));
}
}
@@ -0,0 +1,332 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.TextKey;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
public class CommandPack implements DirectorExecutor {
/**
* Director treats a blank defaultValue as "required", which made the all-packs branch
* unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank
* value (the documented "pack=" escape hatch) still means every pack.
*/
static boolean wantsAllPacks(String pack) {
return pack == null || pack.isBlank() || "*".equals(pack.trim());
}
@Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"})
public void validate(
@Param(description = "The pack folder name to validate, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "*")
String pack
) {
VolmitSender s = sender();
File packsRoot = Iris.instance.getDataFolder("packs");
if (!packsRoot.isDirectory()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACKS_FOLDER_NOT_FOUND));
return;
}
if (wantsAllPacks(pack)) {
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
if (dirs.isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE));
return;
}
int broken = 0;
for (File dir : dirs) {
PackValidationResult result = runValidate(s, dir);
if (result != null && !result.isLoadable()) {
broken++;
}
}
persistValidationCache(packsRoot);
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS, MessageArgument.untrusted("broken", String.valueOf(broken)), MessageArgument.untrusted("value", String.valueOf(dirs.size()))));
return;
}
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
if (target == null) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
runValidate(s, target);
persistValidationCache(packsRoot);
}
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
public void cleanup(
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
String pack,
@Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
if (!result.success()) {
s.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED,
MessageArgument.untrusted("error", String.valueOf(result.error()))
));
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_STILL_QUARANTINED);
return;
}
if (!result.changed()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
PackValidationRegistry.remove(packFolder.getName());
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_MODE_MUST_BE_PREVIEW_APPLY));
return;
}
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
if (!preview.success()) {
s.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED,
MessageArgument.untrusted("error", String.valueOf(preview.error()))
));
return;
}
if (!preview.hasCandidates()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("pack", String.valueOf(pack)), MessageArgument.untrusted("size", String.valueOf(preview.candidatePaths().size()))));
reportPaths(s, preview.candidatePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CANDIDATE);
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_CLEANUP_MODE_APPLY_QUARANTINE_THESE_CANDIDATES_AFTER_FRESH_SCAN, MessageArgument.untrusted("pack", String.valueOf(pack))));
}
@Director(description = "Preview or apply restoration of the latest quarantine", descriptionKey = "iris.director.commandpack.director.preview_apply_restoration_latest_quarantine", aliases = {"r"})
public void restore(
@Param(description = "The pack folder name to restore", descriptionKey = "iris.director.commandpack.param.pack_folder_name_restore")
String pack,
@Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply_2", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
if (!result.conflicts().isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, MessageArgument.untrusted("size", String.valueOf(result.conflicts().size()))));
reportPaths(s, result.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT);
return;
}
if (!result.success()) {
s.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED,
MessageArgument.untrusted("error", String.valueOf(result.error()))
));
return;
}
if (!result.changed()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
PackValidationRegistry.remove(packFolder.getName());
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_MODE_MUST_BE_PREVIEW_APPLY));
return;
}
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
if (!preview.success()) {
s.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED,
MessageArgument.untrusted("error", String.valueOf(preview.error()))
));
return;
}
if (!preview.hasFiles()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("dumpPath", String.valueOf(preview.dumpPath())), MessageArgument.untrusted("size", String.valueOf(preview.filePaths().size()))));
reportPaths(s, preview.filePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_FILE);
if (!preview.conflicts().isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, MessageArgument.untrusted("size", String.valueOf(preview.conflicts().size()))));
reportPaths(s, preview.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT);
return;
}
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_RESTORE_MODE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, MessageArgument.untrusted("pack", String.valueOf(pack))));
}
@Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"})
public void status(
@Param(description = "The pack folder name, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "*")
String pack
) {
VolmitSender s = sender();
if (wantsAllPacks(pack)) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
return;
}
PackValidationRegistry.snapshot().forEach((name, result) -> {
TextKey key = result.isLoadable()
? BukkitRuntimeMessages.COMMAND_PACK_STATUS_OK
: BukkitRuntimeMessages.COMMAND_PACK_STATUS_BROKEN;
s.sendMessage(IrisLanguage.text(
key,
MessageArgument.untrusted("pack", name),
MessageArgument.trusted("blocking", result.getBlockingErrors().size()),
MessageArgument.trusted("warnings", result.getWarnings().size())
));
});
return;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, MessageArgument.untrusted("pack", String.valueOf(pack)), MessageArgument.untrusted("pack2", String.valueOf(pack))));
return;
}
reportResult(s, result);
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
String detail = e.getMessage() == null || e.getMessage().isBlank()
? e.getClass().getSimpleName()
: e.getMessage();
PackValidationResult result = new PackValidationResult(
packFolder.getName(),
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis());
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
}
}
private void persistValidationCache(File packsRoot) {
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
for (File packDirectory : packDirectories) {
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
if (result == null) {
// The boot cache only loads when it covers every pack, so a partial write is
// pointless - but say so instead of silently skipping the persist forever.
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
return;
}
results.add(result);
}
try {
PackValidationCache.save(
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
PackValidationCache.contentFingerprint(packsRoot),
PackValidationCache.contextFingerprint(),
results);
} catch (IOException | RuntimeException e) {
Iris.reportError("Could not persist refreshed pack-validation results", e);
}
}
private void reportResult(VolmitSender s, PackValidationResult result) {
if (result.isLoadable()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_LOADABLE_WARNINGS, MessageArgument.untrusted("packName", String.valueOf(result.getPackName())), MessageArgument.untrusted("size", String.valueOf(result.getWarnings().size()))));
} else {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_BROKEN, MessageArgument.untrusted("packName", String.valueOf(result.getPackName()))));
for (String reason : result.getBlockingErrors()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
}
}
int wMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < wMax; i++) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2, MessageArgument.untrusted("value", String.valueOf(result.getWarnings().get(i)))));
}
if (result.getWarnings().size() > wMax) {
s.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_MORE_WARNING_S,
MessageArgument.trusted("count", result.getWarnings().size() - wMax)
));
}
}
private File findPack(VolmitSender sender, String pack) {
if (pack == null || pack.isBlank()) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_YOU_MUST_SPECIFY_PACK_NAME));
return null;
}
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
if (packFolder == null) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", pack)));
return null;
}
return packFolder;
}
private void reportPaths(VolmitSender sender, List<String> paths, TextKey label) {
int max = Math.min(10, paths.size());
for (int i = 0; i < max; i++) {
sender.sendMessage(IrisLanguage.text(
BukkitCommandMessages.COMMAND_PACK_MESSAGE,
MessageArgument.untrusted("label", IrisLanguage.text(label)),
MessageArgument.untrusted("value", paths.get(i))
));
}
if (paths.size() > max) {
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_MORE, MessageArgument.untrusted("value", (paths.size() - max))));
}
}
}
@@ -0,0 +1,136 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.World;
import org.bukkit.util.Vector;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
public class CommandPregen implements DirectorExecutor {
@Director(description = "Pregenerate a world", descriptionKey = "iris.director.commandpregen.director.pregenerate_world")
public void start(
@Param(description = "The radius of the pregen in blocks", descriptionKey = "iris.director.commandpregen.param.radius_pregen_blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", descriptionKey = "iris.director.commandpregen.param.world_pregen", contextual = true, contextualOverride = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", descriptionKey = "iris.director.commandpregen.param.center_location_pregen_use_me_your_current_location", defaultValue = "0,0")
Vector center,
@Param(description = "Open the Iris pregen gui", descriptionKey = "iris.director.commandpregen.param.open_iris_pregen_gui", defaultValue = "true")
boolean gui,
@Param(name = "serial", description = "Generate only one chunk at a time", descriptionKey = "iris.director.commandpregen.param.generate_only_one_chunk_at_time", defaultValue = "false")
boolean serial
) {
if (radius <= 0) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS));
return;
}
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER));
return;
}
if (PregeneratorJob.getInstance() != null) {
// Reject like the modded adapter does; never silently kill a running job.
sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE));
}
PregenTask task = PregenTask
.builder()
.center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(gui)
.radiusX(radius)
.radiusZ(radius)
.build();
if (serial) {
IrisToolbelt.pregenerateSerial(task, world);
} else {
IrisToolbelt.pregenerate(task, world);
}
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS));
Iris.reportError(e);
}
}
@Director(description = "Stop the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.stop_active_pregeneration_task", aliases = "x")
public void stop() {
if (PregeneratorJob.shutdownInstance()) {
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
sender().sendMessage(message);
Iris.info(message);
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_STOP));
}
}
@Director(description = "Pause / continue the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.pause_continue_active_pregeneration_task", aliases = {"resume"})
public void pause() {
if (PregeneratorJob.pauseResume()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PAUSED_UNPAUSED_PREGENERATION_TASK_NOW, MessageArgument.trusted("value", IrisLanguage.text(PregeneratorJob.isPaused() ? RuntimeUiMessages.STATUS_PAUSED : RuntimeUiMessages.STATUS_RUNNING))));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_PAUSE_UNPAUSE));
}
}
@Director(description = "Show the active pregeneration status", descriptionKey = "iris.director.commandpregen.director.show_active_pregeneration_status")
public void status() {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASK));
return;
}
String world = progress.worldName() == null ? "?" : progress.worldName();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN, MessageArgument.untrusted("world", world), MessageArgument.untrusted("value", Form.f(progress.generated())), MessageArgument.untrusted("value2", Form.f(progress.totalChunks())), MessageArgument.untrusted("value3", String.format("%.1f", progress.percent())), MessageArgument.untrusted("value4", (progress.paused() ? C.YELLOW + " PAUSED" : ""))));
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_PREGEN_SPEED_S_ETA_ELAPSED_METHOD,
MessageArgument.untrusted("overall", Form.f(progress.overallChunksPerSecond(), 1)),
MessageArgument.untrusted("tenSecond", Form.f(progress.chunksPerSecond(), 1)),
MessageArgument.untrusted("thirtySecond", Form.f(progress.thirtySecondChunksPerSecond(), 1)),
MessageArgument.untrusted("sixtySecond", Form.f(progress.sixtySecondChunksPerSecond(), 1)),
MessageArgument.untrusted("eta", Form.duration(progress.eta(), 2)),
MessageArgument.untrusted("elapsed", Form.duration(progress.elapsed(), 2)),
MessageArgument.untrusted("method", progress.method()),
MessageArgument.untrusted("failures", progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : "")));
}
}
@@ -0,0 +1,392 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureCaptureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)", descriptionKey = "iris.director.commandstructure.director.iris_structure_tools_index_import_info")
public class CommandStructure implements DirectorExecutor {
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", descriptionKey = "iris.director.commandstructure.director.regenerate_structure_index_json_listing_all_vanilla_datapack_iris_structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
public void list(
@Param(description = "The dimension whose pack to index", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_index", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
File file = StructureIndexService.write(data);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_WROTE_STRUCTURE_INDEX, MessageArgument.untrusted("value", file.getPath())));
}
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", descriptionKey = "iris.director.commandstructure.director.import_every_structure_vanilla_ingested_datapacks_into_this_pack_as_editable_iris", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
public void importAll(
@Param(description = "The dimension whose pack to import into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_import_into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_2, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORTING_ALL_VANILLA_DATAPACK_STRUCTURES_INTO_OVERWRITE, MessageArgument.untrusted("value", dimension.getLoadKey())));
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
StructureCaptureImporter.Report captured = StructureCaptureImporter.importStructures(
data, StructureImporter.Mode.OVERWRITE, sender(), jigsaws.captureCandidates());
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed)));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS, MessageArgument.untrusted("value", dimension.getLoadKey())));
}
@Director(name = "capture", description = "Capture live registered structures into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. This standalone command overwrites its owned outputs; structures wider/taller than the capture cap and anything that will not generate in a flat overworld are skipped. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. /iris structure import runs a restricted final capture pass only for non-jigsaw structures that have no loadable NBT template.", descriptionKey = "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
public void capture(
@Param(description = "The dimension whose pack to capture into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_capture_into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_3, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_CAPTURED_STRUCTURES_PLACE_THEM_FROM_STRUCTURES_LIST_REGENERATE_CHUNKS_DELETE, MessageArgument.untrusted("value", report.imported())));
}
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", descriptionKey = "iris.director.commandstructure.director.verify_native_structure_eligibility_locate_iris_placed_structures_without_running_blocking_native", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
public void verify(
@Param(description = "The dimension to verify", descriptionKey = "iris.director.commandstructure.param.dimension_verify", aliases = "dim")
IrisDimension dimension,
@Param(description = "Search radius in chunks around the origin (larger is much slower)", descriptionKey = "iris.director.commandstructure.param.search_radius_chunks_around_origin_larger_is_much_slower", defaultValue = "48")
int radius
) {
World world = resolveIrisWorld(dimension);
if (world == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_LOADED_IRIS_WORLD_FOUND_JOIN_CREATE_ONE_FIRST_SEARCH, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
boolean senderIsPlayer = sender() != null && sender().isPlayer();
Location center = senderIsPlayer && player().getWorld() == world
? player().getLocation()
: world.getSpawnLocation();
int searchRadius = Math.max(1, Math.min(radius, 1000));
PlatformChunkGenerator access = IrisToolbelt.access(world);
Engine engine = access == null ? null : access.getEngine();
if (engine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_SELECTED_IRIS_WORLD_HAS_NO_ACTIVE_GENERATOR_ENGINE));
return;
}
KList<String> structureKeys = new KList<>();
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
if (structureRegistry != null) {
for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure);
if (key != null) {
structureKeys.add(key.toString());
}
}
}
for (String placedKey : IrisStructureLocator.placedKeys(engine)) {
if (!structureKeys.contains(placedKey)) {
structureKeys.add(placedKey);
}
}
VolmitSender commandSender = sender();
Player target = senderIsPlayer ? player() : null;
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STRUCTURE_VERIFYING_STRUCTURES_FROM_WITHIN_CHUNKS, MessageArgument.untrusted("value", world.getName()), MessageArgument.untrusted("value2", center.getBlockX()), MessageArgument.untrusted("value3", center.getBlockZ()), MessageArgument.untrusted("searchRadius", searchRadius)));
int centerX = center.getBlockX();
int centerZ = center.getBlockZ();
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
}
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
int searchRadius, VolmitSender commandSender, Player target) {
KList<String> messages = new KList<>();
Map<String, IrisNativeStructureDecision> decisions = new HashMap<>(structureKeys.size());
boolean requiresNativeReachability = false;
for (String keyName : structureKeys) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false);
decisions.put(keyName, decision);
requiresNativeReachability |= !IrisStructureLocator.isPlaced(engine, keyName)
&& decision.generate();
}
Set<String> reachable = Set.of();
if (requiresNativeReachability) {
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
}
}
int located = 0;
int nativeEligible = 0;
int disabled = 0;
int unreachable = 0;
int searchLimited = 0;
int errors = 0;
for (String keyName : structureKeys) {
IrisNativeStructureDecision decision = decisions.get(keyName);
if (IrisStructureLocator.isPlaced(engine, keyName)) {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
searchLimited++;
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
+ ": unable to complete the density search before its safety limit was reached");
continue;
}
if (!result.found()) {
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
continue;
}
located++;
messages.add(C.AQUA + "[iris-planned] " + C.WHITE + keyName + C.GREEN + " @ "
+ result.originX() + "," + result.baseY() + "," + result.originZ());
} catch (Throwable error) {
errors++;
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
+ error.getClass().getSimpleName());
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
}
continue;
}
if (!decision.generate()) {
disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
}
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
unreachable++;
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
continue;
}
nativeEligible++;
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
}
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placement plans found, "
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
sendVerificationMessages(commandSender, target, messages);
}
private void sendVerificationMessages(VolmitSender commandSender, Player target,
KList<String> messages) {
Runnable send = () -> {
for (String message : messages) {
commandSender.sendMessage(message);
}
};
if (target != null) {
J.runEntity(target, send);
} else {
J.s(send);
}
}
private World resolveIrisWorld(IrisDimension dimension) {
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
return player().getWorld();
}
}
for (World w : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(w)) {
continue;
}
PlatformChunkGenerator gen = IrisToolbelt.access(w);
if (matchesDimension(gen, dimension.getLoadKey())) {
return w;
}
}
return null;
}
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
return dimensionKey != null
&& generator != null
&& generator.getEngine() != null
&& generator.getEngine().getDimension() != null
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
}
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", descriptionKey = "iris.director.commandstructure.director.resolve_iris_structure_s_jigsaw_graph_report_piece_count_bounds", origin = DirectorOrigin.BOTH)
public void info(
@Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to inspect", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_inspect")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_4, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK, MessageArgument.untrusted("structure", structure)));
return;
}
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(0, 64, 0));
StructureAssemblyResult assembly = assembler.assemble(new RNG(1234));
List<PlacedStructurePiece> pieces = assembly.pieces();
if (!assembly.hasOutput()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", s.getStartPool())));
return;
}
int minX = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (PlacedStructurePiece p : pieces) {
minX = Math.min(minX, p.getMinX());
minZ = Math.min(minZ, p.getMinZ());
maxX = Math.max(maxX, p.getMaxX());
maxZ = Math.max(maxZ, p.getMaxZ());
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", pieces.size()), MessageArgument.untrusted("value2", (maxX - minX + 1)), MessageArgument.untrusted("value3", (maxZ - minZ + 1))));
}
@Director(description = "Assemble and place an iris structure at your location (studio testing)", descriptionKey = "iris.director.commandstructure.director.assemble_place_iris_structure_at_your_location_studio_testing", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
public void place(
@Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure_2", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to place", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_place")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_5, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2, MessageArgument.untrusted("structure", structure)));
return;
}
Location loc = player().getLocation();
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
StructureAssemblyResult assembly = assembler.assemble(rng);
List<PlacedStructurePiece> pieces = assembly.pieces();
if (!assembly.hasOutput()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, MessageArgument.untrusted("structure", structure)));
return;
}
Map<Block, BlockData> future = new HashMap<>();
World targetWorld = player().getWorld();
PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld);
Engine targetEngine = targetGenerator == null ? null : targetGenerator.getEngine();
IObjectPlacer placer = CommandObject.createPlacer(targetWorld, future, targetEngine);
for (PlacedStructurePiece p : pieces) {
IrisObjectPlacement config = new IrisObjectPlacement();
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
config.setRotation(p.getRotation());
config.getPlace().add(p.getObject().getLoadKey());
if (!s.getEdit().isEmpty()) {
config.setEdit(s.getEdit());
}
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
}
int blockChanges = 0;
for (Map.Entry<Block, BlockData> entry : future.entrySet()) {
if (!entry.getKey().getBlockData().equals(entry.getValue())) {
blockChanges++;
}
}
if (blockChanges == 0) {
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", pieces.size())));
return;
}
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", pieces.size()),
MessageArgument.untrusted("value2", blockChanges)));
}
}
@@ -0,0 +1,964 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.ImageMapStudioGUI;
import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.FeatureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisNoiseGenerator;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
public class CommandStudio implements DirectorExecutor {
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
public static String hrf(Duration duration) {
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
}
@Director(description = "Open a new studio world", descriptionKey = "iris.director.commandstudio.director.open_new_studio_world", aliases = "o", sync = true)
public void open(
@Param(description = "The dimension pack to open a studio for", descriptionKey = "iris.director.commandstudio.param.dimension_pack_open_studio", aliases = "dim", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandstudio.param.seed_generate_studio_with", aliases = "s")
long seed,
@Param(defaultValue = "false", description = "Attempt to open Studio before a required registry restart", aliases = "f")
boolean force) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("seed", seed)));
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey(), force);
}
@Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", descriptionKey = "iris.director.commandstudio.director.import_vanilla_trees_mushrooms_objects_structures_jigsaw_from_server_into_pack_s", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH)
public void importvanilla(
@Param(description = "The dimension pack to import vanilla content into", descriptionKey = "iris.director.commandstudio.param.dimension_pack_import_vanilla_content_into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(description = "How many variants to capture per tree/object feature", descriptionKey = "iris.director.commandstudio.param.how_many_variants_capture_per_tree_object_feature", defaultValue = "3")
int variants,
@Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", descriptionKey = "iris.director.commandstudio.param.also_import_vanilla_datapack_structures_jigsaw_into_pack", defaultValue = "true")
boolean structures
) {
if (dimension == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION));
return;
}
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
return;
}
VolmitSender sender = sender();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTING_VANILLA_CONTENT_INTO, MessageArgument.untrusted("value", dimension.getLoadKey())));
FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender);
int imported = features.imported();
int failed = features.failed();
if (structures) {
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender);
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender);
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender);
imported += jigsaws.imported() + templates.imported() + groups.imported();
failed += jigsaws.failed() + templates.failed() + groups.failed();
}
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTVANILLA_COMPLETE_OBJECTS_STRUCTURES_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed)));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME));
}
@Director(description = "Open VSCode for a dimension", descriptionKey = "iris.director.commandstudio.director.open_vscode_dimension", aliases = {"vsc"})
public void vscode(
@Param(defaultValue = "default", description = "The dimension to open VSCode for", descriptionKey = "iris.director.commandstudio.param.dimension_open_vscode", aliases = "dim", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_VSCODE_PACK, MessageArgument.untrusted("value", dimension.getName())));
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
}
@Director(description = "Close an open studio project", descriptionKey = "iris.director.commandstudio.director.close_open_studio_project", aliases = {"x"}, sync = true)
public void close() {
VolmitSender commandSender = sender();
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS));
return;
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_CLOSING_STUDIO));
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
if (throwable != null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", String.valueOf(throwable.getMessage()))));
return;
}
if (result != null && result.failureCause() != null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2, MessageArgument.untrusted("value", String.valueOf(result.failureCause().getMessage()))));
return;
}
if (result != null && result.startupCleanupQueued()) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK));
return;
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED));
}));
}
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
public void scoreboard() {
Player target = player();
VolmitSender commandSender = sender();
if (!J.runEntity(target, () -> {
PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld());
if (generator == null || !generator.isStudio()) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD_TOGGLE_DEBUG_SCOREBOARD));
return;
}
boolean visible = Iris.service(BoardSVC.class).toggle(target);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_DEBUG_SCOREBOARD, MessageArgument.trusted("value", visible ? C.GREEN : C.YELLOW), MessageArgument.trusted("value2", IrisLanguage.text(visible ? RuntimeUiMessages.STATUS_ENABLED : RuntimeUiMessages.STATUS_DISABLED))));
})) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_COULD_NOT_UPDATE_STUDIO_DEBUG_SCOREBOARD_RIGHT_NOW));
}
}
@Director(description = "Create a new studio project", descriptionKey = "iris.director.commandstudio.director.create_new_studio_project", aliases = "+")
public void create(
@Param(description = "The name of this new Iris Project.", descriptionKey = "iris.director.commandstudio.param.name_this_new_iris_project", defaultValue = "studio")
String name,
@Param(
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", descriptionKey = "iris.director.commandstudio.param.copy_contents_existing_project_your_packs_folder_use_it_as_template_this",
defaultValue = "null",
contextual = true,
contextualOverride = true,
customHandler = NullableDimensionHandler.class
)
IrisDimension template) {
if (template != null) {
Iris.service(StudioSVC.class).create(sender(), name, template);
} else {
Iris.service(StudioSVC.class).create(sender(), name);
}
}
@Director(description = "Get the version of a pack", descriptionKey = "iris.director.commandstudio.director.get_version_pack")
public void version(
@Param(defaultValue = "default", description = "The dimension get the version of", descriptionKey = "iris.director.commandstudio.param.dimension_get_version", aliases = "dim", contextual = true, contextualOverride = true, customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PACK_HAS_VERSION, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("value2", dimension.getVersion())));
}
@Director(description = "Open the noise explorer (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_noise_explorer_external_gui", aliases = {"nmap"})
public void noise(
@Param(description = "Optional pack generator to preview", descriptionKey = "iris.director.commandstudio.param.optional_pack_generator_preview", defaultValue = "null", contextual = true, contextualOverride = true)
IrisGenerator generator,
@Param(description = "The seed to preview the generator with", descriptionKey = "iris.director.commandstudio.param.seed_preview_generator_with", defaultValue = "12345")
long seed
) {
if (noGUI()) return;
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
if (generator == null) {
NoiseExplorerGUI.launch();
return;
}
String generatorKey = generator.getLoadKey();
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
}
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
public void imagemap(
@Param(name = "world", description = "The Iris world whose active pack receives image-map exports", descriptionKey = "iris.director.commandstudio.param.world_open_image_map_studio", contextual = true, contextualOverride = true)
World world
) {
if (noGUI()) {
return;
}
if (!ImageMapStudioGUI.isAvailable()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return;
}
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
return;
}
Engine activeEngine = IrisToolbelt.access(world).getEngine();
ImageMapStudioGUI.launch(activeEngine);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
}
@Director(description = "Show loot if a chest were right here", descriptionKey = "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", origin = DirectorOrigin.PLAYER, sync = true)
public void loot(
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", descriptionKey = "iris.director.commandstudio.param.fast_insertion_items_virtual_inventory_may_cause_performance_drop", defaultValue = "false")
boolean fast,
@Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", descriptionKey = "iris.director.commandstudio.param.whether_not_append_inventory_currently_open_if_false_clears_opened_inventory", defaultValue = "true")
boolean add
) {
if (noStudio()) return;
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock());
Inventory inv = Bukkit.createInventory(null, 27 * 2);
try {
EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ());
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
return;
}
O<Integer> ta = new O<>();
ta.set(-1);
var sender = sender();
var player = player();
var engine = engine();
ta.set(J.sr(() ->
{
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
J.csr(ta.get());
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
return;
}
if (!add) {
inv.clear();
}
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
}, fast ? 5 : 35));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
player().openInventory(inv);
}
@Director(description = "Calculate the chance for each region to generate", descriptionKey = "iris.director.commandstudio.director.calculate_chance_each_region_generate", origin = DirectorOrigin.PLAYER)
public void regions(@Param(description = "The radius in chunks", descriptionKey = "iris.director.commandstudio.param.radius_chunks", defaultValue = "500") int radius) {
var engine = engine();
if (engine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
return;
}
if (radius <= 0 || radius > 2048) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE));
return;
}
var sender = sender();
var player = player();
Thread.ofVirtual()
.start(() -> {
int d = radius * 2;
KMap<String, AtomicInteger> data = new KMap<>();
// Everything acquired below is released in the finally: a throw mid-scan
// previously leaked the whole sampler ForkJoinPool, the self-rescheduling
// progress task and both HUD claims for the rest of the server's uptime.
MultiBurst multiBurst = null;
boolean progressShown = false;
int c = -1;
try {
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
multiBurst = new MultiBurst("Region Sampler");
var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
var loc = player.getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
progressShown = true;
c = J.ar(() -> {
double jobProgress = (double) completedTasks.get() / totalTasks;
sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS));
BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L);
}, 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
.incrementAndGet();
completedTasks.incrementAndGet();
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
executor.complete();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE));
var loader = engine.getData().getRegionLoader();
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
} catch (Throwable e) {
Iris.reportError(e);
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_SCAN_FAILED,
MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
} finally {
if (c != -1) {
J.car(c);
}
if (progressShown) {
sender.sendAction(" ");
BukkitPlatform.hudLanes().hide(player, "iris:job");
}
if (multiBurst != null) {
multiBurst.close();
}
}
});
}
@Director(description = "Render a world map (External GUI)", descriptionKey = "iris.director.commandstudio.director.render_world_map_external_gui", aliases = "render")
public void map(
@Param(name = "world", description = "The world to open the generator for", descriptionKey = "iris.director.commandstudio.param.world_open_generator", contextual = true, contextualOverride = true)
World world
) {
if (noGUI()) return;
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
return;
}
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
}
@Director(description = "Package a dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
public void pkg(
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
boolean obfuscate,
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
boolean minify
) {
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
}
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
public void profile(
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
// Todo: Make this more accurate
File pack = dimension.getLoadFile().getParentFile().getParentFile();
File report = Iris.instance.getDataFile("profile.txt");
IrisProject project = new IrisProject(pack);
IrisData data = IrisData.get(pack);
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
if (activeEngine != null) {
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
} else {
IrisToolbelt.applyPregenPerformanceProfile();
}
KList<String> fileText = new KList<>();
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
KMap<String, Double> generatorTimings = new KMap<>();
KMap<String, Double> biomeTimings = new KMap<>();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS));
for (NoiseStyle i : NoiseStyle.values()) {
CNG c = i.create(new RNG(i.hashCode()));
for (int j = 0; j < 3000; j++) {
c.noise(j, j + 1000, j * j);
c.noise(j, -j);
}
PrecisionStopwatch px = PrecisionStopwatch.start();
for (int j = 0; j < 100000; j++) {
c.noise(j, j + 1000, j * j);
c.noise(j, -j);
}
styleTimings.put(i, px.getMilliseconds());
}
fileText.add("Noise Style Performance Impacts: ");
for (NoiseStyle i : styleTimings.sortKNumber()) {
fileText.add(i.name() + ": " + styleTimings.get(i));
}
fileText.add("");
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_INTERPOLATOR_TIMINGS));
for (InterpolationMethod i : InterpolationMethod.values()) {
IrisInterpolator in = new IrisInterpolator();
in.setFunction(i);
in.setHorizontalScale(8);
NoiseProvider np = (x, z) -> Math.random();
for (int j = 0; j < 3000; j++) {
in.interpolate(j, -j, np);
}
PrecisionStopwatch px = PrecisionStopwatch.start();
for (int j = 0; j < 100000; j++) {
in.interpolate(j + 10000, -j - 100000, np);
}
interpolatorTimings.put(i, px.getMilliseconds());
}
fileText.add("Noise Interpolator Performance Impacts: ");
for (InterpolationMethod i : interpolatorTimings.sortKNumber()) {
fileText.add(i.name() + ": " + interpolatorTimings.get(i));
}
fileText.add("");
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROCESSING_GENERATOR_SCORES));
KMap<String, KList<String>> btx = new KMap<>();
for (String i : data.getGeneratorLoader().getPossibleKeys()) {
KList<String> vv = new KList<>();
IrisGenerator g = data.getGeneratorLoader().load(i);
KList<IrisNoiseGenerator> composites = g.getAllComposites();
double score = 0;
int m = 0;
for (IrisNoiseGenerator j : composites) {
m++;
score += styleTimings.get(j.getStyle().getStyle());
vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle()));
}
score += interpolatorTimings.get(g.getInterpolator().getFunction());
vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction()));
generatorTimings.put(i, score);
btx.put(i, vv);
}
fileText.add("Project Generator Performance Impacts: ");
for (String i : generatorTimings.sortKNumber()) {
fileText.add(i + ": " + generatorTimings.get(i));
btx.get(i).forEach((ii) -> fileText.add(" " + ii));
}
fileText.add("");
KMap<String, KList<String>> bt = new KMap<>();
for (String i : data.getBiomeLoader().getPossibleKeys()) {
KList<String> vv = new KList<>();
IrisBiome b = data.getBiomeLoader().load(i);
double score = 0;
int m = 0;
for (IrisBiomePaletteLayer j : b.getLayers()) {
m++;
score += styleTimings.get(j.getStyle().getStyle());
vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle()));
}
score += styleTimings.get(b.getBiomeStyle().getStyle());
vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle()));
score += styleTimings.get(b.getChildStyle().getStyle());
vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle()));
biomeTimings.put(i, score);
bt.put(i, vv);
}
fileText.add("Project Biome Performance Impacts: ");
for (String i : biomeTimings.sortKNumber()) {
fileText.add(i + ": " + biomeTimings.get(i));
bt.get(i).forEach((ff) -> fileText.add(" " + ff));
}
fileText.add("");
double m = 0;
for (double i : biomeTimings.v()) {
m += i;
}
m /= biomeTimings.size();
double mm = 0;
for (double i : generatorTimings.v()) {
mm += i;
}
mm /= generatorTimings.size();
m += mm;
fileText.add("Average Score: " + m);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0))));
try {
IO.writeAll(report, fileText.toString("\n"));
} catch (IOException e) {
Iris.reportError(e);
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
}
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null) {
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
if (isGeneratorDimension(activeProvider, dimension)) {
return activeProvider;
}
}
}
if (!sender().isPlayer()) {
return null;
}
Player player = sender().player();
if (player == null) {
return null;
}
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
if (isGeneratorDimension(worldAccess, dimension)) {
return worldAccess;
}
return null;
}
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
return false;
}
IrisDimension engineDimension = generator.getEngine().getDimension();
if (engineDimension == null || engineDimension.getLoadKey() == null) {
return false;
}
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
}
@Director(description = "Spawn an Iris entity", descriptionKey = "iris.director.commandstudio.director.spawn_iris_entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
public void spawn(
@Param(description = "The entity to spawn", descriptionKey = "iris.director.commandstudio.param.entity_spawn")
IrisEntity entity,
@Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true, contextualOverride = true)
Vector location
) {
VolmitSender commandSender = sender();
Engine spawnEngine = engine();
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
}
// Entity creation must run on the thread owning the destination chunk.
Location at = new Location(world(), location.getX(), location.getY(), location.getZ());
if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) {
Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + ".");
}
}
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
public void tpstudio() {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (!studioService.isProjectOpen()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN));
return;
}
if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_ARE_ALREADY_STUDIO_WORLD));
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD));
Player player = player();
studioService.teleportToActiveProject(player)
.whenComplete((teleported, failure) -> {
if (failure != null) {
Iris.reportError("Studio teleport failed for player \"" + player.getName() + "\".", failure);
return;
}
});
}
@Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace")
public void update(
@Param(description = "The dimension to update the workspace of", descriptionKey = "iris.director.commandstudio.param.dimension_update_workspace", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName())));
}
}
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks", descriptionKey = "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks", origin = DirectorOrigin.PLAYER)
public void objects() {
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD));
return;
}
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD_2));
return;
}
KList<Chunk> chunks = new KList<>();
Player reporter = player();
CountDownLatch gathered = new CountDownLatch(1);
// The raycast and the chunk loads need the thread owning the player; the report itself stays off it.
boolean scheduled = J.runEntity(reporter, () -> {
try {
int bx = reporter.getLocation().getChunk().getX();
int bz = reporter.getLocation().getChunk().getZ();
try {
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
int cx = l.getChunk().getX();
int cz = l.getChunk().getZ();
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
} catch (Throwable e) {
Iris.reportError(e);
}
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
} finally {
gathered.countDown();
}
});
if (!scheduled) {
Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + ".");
return;
}
try {
if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + ".");
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
try (PrintWriter pw = new PrintWriter(ff)) {
pw.println("=== Iris Chunk Report ===");
pw.println("== General Info ==");
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
pw.println("MC Version: " + Bukkit.getVersion());
pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!"));
pw.println("Report Captured At: " + new Date());
pw.println("Chunks: (" + chunks.size() + "): ");
for (Chunk i : chunks) {
pw.println("- [" + i.getX() + ", " + i.getZ() + "]");
}
int regions = 0;
long size = 0;
String age = "No idea...";
try {
for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) {
if (i.isFile()) {
size += i.length();
}
}
} catch (Throwable e) {
Iris.reportError(e);
}
try {
FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime");
age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS));
} catch (IOException e) {
Iris.reportError(e);
}
KList<String> biomes = new KList<>();
KList<String> caveBiomes = new KList<>();
KMap<String, KMap<String, KList<String>>> objects = new KMap<>();
for (Chunk i : chunks) {
for (int j = 0; j < 16; j += 3) {
for (int k = 0; k < 16; k += 3) {
assert engine() != null;
IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")");
caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")");
exportObjects(bb, pw, engine(), objects);
exportObjects(bxf, pw, engine(), objects);
}
}
}
String[] regionFiles = new File(world.getWorldFolder(), "region").list();
regions = regionFiles == null ? 0 : regionFiles.length;
pw.println();
pw.println("== World Info ==");
pw.println("World Name: " + world.getName());
pw.println("Age: " + age);
pw.println("Folder: " + world.getWorldFolder().getPath());
pw.println("Regions: " + Form.f(regions));
pw.println("Chunks: max. " + Form.f(regions * 32 * 32));
pw.println("World Size: min. " + Form.fileSize(size));
pw.println();
pw.println("== Biome Info ==");
pw.println("Found " + biomes.size() + " Biome(s): ");
for (String i : biomes) {
pw.println("- " + i);
}
pw.println();
pw.println("== Object Info ==");
for (String i : objects.k()) {
pw.println("- " + i);
for (String j : objects.get(i).k()) {
pw.println(" @ " + j);
for (String k : objects.get(i).get(j)) {
pw.println(" * " + k);
}
}
}
pw.println();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
} catch (Throwable e) {
Iris.reportError(e);
}
}
private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap<String, KMap<String, KList<String>>> objects) {
String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")";
int m = 0;
KSet<String> stop = new KSet<>();
for (IrisObjectPlacement f : bb.getObjects()) {
m++;
String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)";
for (String i : f.getPlace()) {
String nn3 = i + ": [ERROR] Failed to find object!";
try {
if (stop.contains(i)) {
continue;
}
File ff = g.getData().getObjectLoader().findFile(i);
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
stop.add(i);
} catch (Throwable e) {
Iris.reportError(e);
}
String n3 = nn3;
objects.computeIfAbsent(n1, (k1) -> new KMap<>())
.computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3);
}
}
}
/**
* @return true if server GUIs are not enabled
*/
private boolean noGUI() {
if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return true;
}
return false;
}
/**
* @return true if no studio is open or the player is not in one
*/
private boolean noStudio() {
if (!sender().isPlayer()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PLAYERS_ONLY));
return true;
}
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2));
return true;
}
if (!IrisToolbelt.isStudio(world())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD));
return true;
}
return false;
}
public void files(File clean, KList<File> files) {
if (clean.isDirectory()) {
for (File i : clean.listFiles()) {
files(i, files);
}
} else if (clean.getName().endsWith(".json")) {
try {
files.add(clean);
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
}
}
}
private void fixBlocks(JSONObject obj) {
for (String i : obj.keySet()) {
Object o = obj.get(i);
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
obj.put(i, "minecraft:" + o);
}
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
private void fixBlocks(JSONArray obj) {
for (int i = 0; i < obj.length(); i++) {
Object o = obj.get(i);
if (o instanceof JSONObject) {
fixBlocks((JSONObject) o);
} else if (o instanceof JSONArray) {
fixBlocks((JSONArray) o);
}
}
}
}
@@ -0,0 +1,290 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import java.lang.reflect.Method;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?", descriptionKey = "iris.director.commandwhat.director.iris_what")
public class CommandWhat implements DirectorExecutor {
@Director(description = "What is in my hand?", descriptionKey = "iris.director.commandwhat.director.what_is_my_hand", origin = DirectorOrigin.PLAYER)
public void hand() {
try {
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
if (!bd.getMaterial().equals(Material.AIR)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL, MessageArgument.untrusted("value", bd.getMaterial().name())));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL, MessageArgument.untrusted("value", bd.getAsString(true))));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM));
}
} catch (Throwable e) {
Iris.reportError(e);
Material bd = player().getInventory().getItemInMainHand().getType();
if (!bd.equals(Material.AIR)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_2, MessageArgument.untrusted("value", bd.name())));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM_2));
}
}
}
@Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER)
public void biome() {
try {
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
Biome derivative = b.getDerivative();
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
} catch (Throwable e) {
Iris.reportError(e);
Biome biome = player().getLocation().getBlock().getBiome();
NamespacedKey key = resolveBiomeKey(biome);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
if (key == null || key.getKey().equals("custom")) {
try {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(player().getLocation())), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())))));
} catch (Throwable ee) {
Iris.reportError(ee);
}
}
}
}
@Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER)
public void region() {
VolmitSender commandSender = sender();
Player player = player();
World world = world();
Engine engine = engine();
// Chunk access must happen on the thread owning the player's chunk.
onPlayerThread(player, () -> {
try {
Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
} catch (Throwable e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
}
});
}
@Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER)
public void block() {
VolmitSender commandSender = sender();
Player player = player();
// The raycast reads blocks, so it has to run on the thread owning the player.
onPlayerThread(player, () -> {
BlockData bd;
try {
bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
} catch (NullPointerException e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
bd = null;
}
if (bd != null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
if (BukkitBlockResolution.isStorage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
}
if (BukkitBlockResolution.isLit(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
}
if (BukkitBlockResolution.isFoliage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isDecorant(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
}
if (BukkitBlockResolution.isFluid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
}
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isSolid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
}
}
});
}
// Matches ModdedWhatCommands.MAX_MARKERS so both loaders truncate identically.
private static final int MAX_MARKER_HITS = 8_192;
private static final int MARKER_SIGNAL_BATCH = 128;
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
VolmitSender commandSender = sender();
Player player = player();
if (player == null) {
return;
}
// Only the chunk coordinates need the owning thread; the 81-chunk mantle sweep loads
// tectonic plates from disk and must never run on the tick thread (the modded adapter
// already scans async, leased and capped — mirror it).
onPlayerThread(player, () -> {
World world = player.getWorld();
Chunk c = player.getLocation().getChunk();
int chunkX = c.getX();
int chunkZ = c.getZ();
if (!IrisToolbelt.isIrisWorld(world)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
J.a(() -> {
KList<Location> hits = new KList<>();
MatterMarker matterMarker = new MatterMarker(marker);
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
scan:
for (int xxx = chunkX - 4; xxx <= chunkX + 4; xxx++) {
for (int zzz = chunkZ - 4; zzz <= chunkZ + 4; zzz++) {
for (Location i : engine.getMantle().findMarkers(xxx, zzz, matterMarker)
.convert((p) -> BukkitPlatform.toLocation(p, world))) {
hits.add(i);
if (hits.size() >= MAX_MARKER_HITS) {
break scan;
}
}
}
}
} catch (GenerationSessionException e) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
return;
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker)));
emitMarkerSignals(player, engine, hits, 0);
});
});
}
private void emitMarkerSignals(Player player, Engine engine, KList<Location> hits, int from) {
if (from >= hits.size() || !player.isOnline() || engine.isClosed() || engine.isClosing()) {
return;
}
int to = Math.min(from + MARKER_SIGNAL_BATCH, hits.size());
for (int i = from; i < to; i++) {
Location at = hits.get(i);
BlockSignal.of(at.getWorld(), at.getBlockX(), at.getBlockY(), at.getBlockZ(), 100);
}
J.s(() -> emitMarkerSignals(player, engine, hits, to), 1);
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + ".");
}
}
private NamespacedKey resolveBiomeKey(Biome biome) {
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
Object keyValue = invokeNoThrow(biome, "getKey");
if (keyValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
return null;
}
private Object invokeNoThrow(Biome biome, String methodName) {
if (biome == null) {
return null;
}
try {
Method method = biome.getClass().getMethod(methodName);
return method.invoke(biome);
} catch (Throwable ignored) {
return null;
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.util.common.math.IrisBlockVector;
import java.util.Objects;
record ObjectPasteBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
static ObjectPasteBounds resolve(
IrisObject object,
IrisObjectRotation rotation,
int anchorX,
int anchorY,
int anchorZ
) {
IrisObject placedObject = Objects.requireNonNull(object, "object");
IrisObjectRotation placementRotation = Objects.requireNonNull(rotation, "rotation");
int[] xOffsets = {
-placedObject.getCenter().getBlockX(),
placedObject.getW() - 1 - placedObject.getCenter().getBlockX()
};
int[] yOffsets = {
-placedObject.getCenter().getBlockY(),
placedObject.getH() - 1 - placedObject.getCenter().getBlockY()
};
int[] zOffsets = {
-placedObject.getCenter().getBlockZ(),
placedObject.getD() - 1 - placedObject.getCenter().getBlockZ()
};
int minimumX = Integer.MAX_VALUE;
int minimumY = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumY = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int xOffset : xOffsets) {
for (int yOffset : yOffsets) {
for (int zOffset : zOffsets) {
IrisBlockVector transformed = placementRotation.rotate(new IrisBlockVector(xOffset, yOffset, zOffset));
int worldX = anchorX + (int) Math.round(transformed.getX());
int worldY = anchorY + (int) Math.round(transformed.getY());
int worldZ = anchorZ + (int) Math.round(transformed.getZ());
minimumX = Math.min(minimumX, worldX);
minimumY = Math.min(minimumY, worldY);
minimumZ = Math.min(minimumZ, worldZ);
maximumX = Math.max(maximumX, worldX);
maximumY = Math.max(maximumY, worldY);
maximumZ = Math.max(maximumZ, worldZ);
}
}
}
return new ObjectPasteBounds(minimumX, minimumY, minimumZ, maximumX, maximumY, maximumZ);
}
}
@@ -0,0 +1,56 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.CommandSVC;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import org.bukkit.command.CommandSender;
import java.util.Collection;
import java.util.List;
/**
* Paper-only command registration. Lives outside {@code art.arcane.iris.core.service} on purpose:
* that package is eagerly Class.forName-scanned by JarScanner during enable, and resolving this
* class on plain Spigot throws NoClassDefFoundError for the Paper lifecycle types. CommandSVC
* loads it reflectively only when the Bukkit plugin.yml command is unavailable (Paper path).
*/
public final class PaperCommandRegistrar {
private static final String ROOT_COMMAND = "iris";
private PaperCommandRegistrar() {
}
public static void register(Iris plugin, CommandSVC commandService) {
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
event.registrar().register(
ROOT_COMMAND,
"Iris world generation command.",
List.of("ir", "irs"),
command(commandService)
)
);
}
static BasicCommand command(CommandSVC commandService) {
return new PaperCommand(commandService);
}
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
@Override
public void execute(CommandSourceStack source, String[] args) {
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public Collection<String> suggest(CommandSourceStack source, String[] args) {
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public boolean canUse(CommandSender sender) {
return true;
}
}
}
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.edit;
package art.arcane.iris.core.edit;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@@ -16,28 +16,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.edit;
package art.arcane.iris.core.edit;
import com.volmit.iris.util.math.M;
import art.arcane.volmlib.util.math.M;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@SuppressWarnings("ClassCanBeRecord")
public class BukkitBlockEditor implements BlockEditor {
private final World world;
private volatile long last;
public BukkitBlockEditor(World world) {
this.world = world;
this.last = M.ms();
}
@Override
public void set(int x, int y, int z, BlockData d) {
touch();
world.getBlockAt(x, y, z).setBlockData(d, false);
}
@Override
public BlockData get(int x, int y, int z) {
touch();
return world.getBlockAt(x, y, z).getBlockData();
}
@@ -48,28 +51,38 @@ public class BukkitBlockEditor implements BlockEditor {
@Override
public long last() {
return M.ms();
return last;
}
@SuppressWarnings("deprecation")
@Override
public void setBiome(int x, int z, Biome b) {
world.setBiome(x, z, b);
touch();
int minHeight = world.getMinHeight();
int maxHeight = world.getMaxHeight();
for (int y = minHeight; y < maxHeight; y++) {
world.setBiome(x, y, z, b);
}
}
@Override
public void setBiome(int x, int y, int z, Biome b) {
touch();
world.setBiome(x, y, z, b);
}
@Override
public Biome getBiome(int x, int y, int z) {
touch();
return world.getBiome(x, y, z);
}
@SuppressWarnings("deprecation")
@Override
public Biome getBiome(int x, int z) {
return world.getBiome(x, z);
touch();
return world.getBiome(x, world.getMinHeight(), z);
}
private void touch() {
last = M.ms();
}
}
@@ -0,0 +1,92 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider {
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
public static void install() {
GuiHost.set(new BukkitGuiHost());
}
@Override
public Engine findActiveEngine() {
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return null;
}
@Override
public void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
hotloadHooks.put(onHotload, listener);
Iris.instance.registerListener(listener);
}
@Override
public void unregisterHotloadHook(Runnable onHotload) {
Listener listener = hotloadHooks.remove(onHotload);
if (listener != null) {
Iris.instance.unregisterListener(listener);
}
}
@Override
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
return engine == null ? null : new BukkitVisionOverlay(engine, openerId);
}
private static final class HotloadListener implements Listener {
private final Runnable onHotload;
private HotloadListener(Runnable onHotload) {
this.onHotload = onHotload;
}
@EventHandler
public void on(IrisEngineHotloadEvent event) {
onHotload.run();
}
}
}
@@ -0,0 +1,345 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
private final UUID openerId;
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private final AtomicLong teleportSequence = new AtomicLong();
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine, UUID openerId) {
this.engine = engine;
this.openerId = openerId;
}
/**
* Called from the AWT event thread, so it may only hand back the last snapshot
* built by a server thread.
*/
@Override
public List<GuiMarker> players() {
queuePlayerRefresh();
return playerMarkers;
}
private void queuePlayerRefresh() {
if (!playerRefreshQueued.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(() -> {
try {
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
Location at = player.getLocation();
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ()));
}
playerMarkers = List.copyOf(markers);
} finally {
playerRefreshQueued.set(false);
}
});
if (!scheduled) {
playerRefreshQueued.set(false);
}
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
World world = BukkitWorldBinding.world(target);
if (world == null) {
sink.accept(List.of());
return;
}
List<LivingEntity> living = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) {
if (!(entity instanceof Player)) {
living.add(entity);
}
}
if (living.isEmpty()) {
sink.accept(List.of());
return;
}
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
AtomicInteger pending = new AtomicInteger(living.size());
Runnable complete = () -> {
if (pending.decrementAndGet() == 0) {
sink.accept(List.copyOf(collected));
}
};
for (LivingEntity entity : living) {
Location at = entity.getLocation();
Runnable read = () -> {
try {
collected.add(marker(entity, at));
} catch (Throwable ignored) {
} finally {
complete.run();
}
};
if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) {
complete.run();
}
}
});
}
private GuiMarker marker(LivingEntity entity, Location at) {
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
}
@Override
public void teleport(double worldX, double worldZ) {
VisionTeleportRequest request = new VisionTeleportRequest(
teleportSequence.incrementAndGet(),
VisionGUI.floorWorldCoordinate(worldX),
VisionGUI.floorWorldCoordinate(worldZ));
latestTeleport.set(request);
startTeleport(request);
}
private void startTeleport(VisionTeleportRequest request) {
if (!request.processing.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
if (!isCurrent(request, target)) {
finish(request);
return;
}
World world = BukkitWorldBinding.world(target);
if (world == null) {
finish(request);
return;
}
List<Player> players = BukkitWorldBinding.players(target);
Player player = selectPlayer(players);
if (player == null) {
finish(request);
return;
}
int blockX = request.blockX;
int blockZ = request.blockZ;
try {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
Location destination = new Location(
world,
blockX + 0.5D,
blockY,
blockZ + 0.5D);
if (!J.runEntity(player, () -> delegateTeleport(
request,
target,
player,
world,
destination))) {
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision teleport on the player entity."));
}
} catch (Throwable failure) {
fail(request, target, world, failure);
}
});
if (!scheduled) {
finish(request);
}
}
private Player selectPlayer(List<Player> players) {
if (openerId == null) {
return players.isEmpty() ? null : players.get(0);
}
for (Player player : players) {
if (openerId.equals(player.getUniqueId())) {
return player;
}
}
return null;
}
private void delegateTeleport(
VisionTeleportRequest request,
IrisWorld target,
Player player,
World world,
Location destination
) {
if (!isCurrent(request, target) || !player.isOnline() || player.getWorld() != world) {
finish(request);
return;
}
if (!nativeTeleportActive.compareAndSet(false, true)) {
finish(request);
return;
}
if (!isCurrent(request, target)) {
nativeTeleportActive.set(false);
finish(request);
restartLatest(request);
return;
}
CompletableFuture<Boolean> teleport;
try {
teleport = BukkitPlatform.teleportAsync(player, destination);
} catch (Throwable failure) {
nativeTeleportActive.set(false);
fail(request, target, world, failure);
restartLatest(request);
return;
}
if (teleport == null) {
nativeTeleportActive.set(false);
fail(request, target, world, new IllegalStateException(
"Vision teleport returned no completion future."));
restartLatest(request);
return;
}
teleport.whenComplete((success, failure) -> {
nativeTeleportActive.set(false);
finish(request);
if (isCurrent(request, target)) {
if (failure != null) {
reportTeleportFailure(world, request.blockX, request.blockZ, failure);
} else if (!Boolean.TRUE.equals(success)) {
reportTeleportFailure(world, request.blockX, request.blockZ, new IllegalStateException(
"Vision teleport did not complete successfully."));
}
}
restartLatest(request);
});
}
private boolean isCurrent(VisionTeleportRequest request, IrisWorld target) {
VisionTeleportRequest current = latestTeleport.get();
return current != null
&& current.sequence == request.sequence
&& target != null
&& engine.getWorld() == target
&& target.hasPlatformWorld()
&& !engine.isClosing()
&& !engine.isClosed();
}
private void fail(
VisionTeleportRequest request,
IrisWorld target,
World world,
Throwable failure
) {
finish(request);
if (isCurrent(request, target)) {
reportTeleportFailure(world, request.blockX, request.blockZ, failure);
}
}
private void finish(VisionTeleportRequest request) {
request.processing.set(false);
}
private void restartLatest(VisionTeleportRequest completed) {
VisionTeleportRequest current = latestTeleport.get();
if (current != null && current != completed) {
startTeleport(current);
}
}
private void reportTeleportFailure(World world, int blockX, int blockZ, Throwable failure) {
IrisLogging.reportError("Vision could not teleport to " + world.getName() + "@"
+ blockX + "," + blockZ + ".", failure);
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
private static final class VisionTeleportRequest {
private final long sequence;
private final int blockX;
private final int blockZ;
private final AtomicBoolean processing;
private VisionTeleportRequest(long sequence, int blockX, int blockZ) {
this.sequence = sequence;
this.blockX = blockX;
this.blockZ = blockZ;
processing = new AtomicBoolean(false);
}
}
}
@@ -0,0 +1,59 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry;
import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion;
import java.util.Objects;
import java.util.logging.Logger;
public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
public static final String IDENTIFIER = "iris";
public static final String AUTHOR = "Volmit Software";
public static final String VERSION = "2.0.0";
public static final String REQUIRED_PLUGIN = "Iris";
public IrisPapiExpansion(IrisPapiState state, Logger logger) {
super(IDENTIFIER, AUTHOR, VERSION, REQUIRED_PLUGIN, registry(state), logger);
}
public static PlaceholderKeyRegistry registry(IrisPapiState state) {
Objects.requireNonNull(state, "state");
return PlaceholderKeyRegistry.builder()
.key("available", state::available)
.key("world.available", state::worldAvailable)
.key("world.biome", state::biome)
.key("world.biome-key", state::biomeKey)
.key("world.region", state::region)
.key("world.region-key", state::regionKey)
.key("world.dimension", state::dimension)
.key("pregen.available", state::pregenAvailable)
.key("pregen.world", state::pregenWorld)
.key("pregen.percent", state::pregenPercent)
.key("pregen.eta", state::pregenEta)
.key("pregen.eta-text", state::pregenEtaText)
.key("pregen.chunks", state::pregenChunks)
.key("pregen.total", state::pregenTotal)
.key("pregen.chunks-per-second", state::pregenChunksPerSecond)
.key("pregen.paused", state::pregenPaused)
.build();
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import java.util.logging.Logger;
public final class IrisPapiInstaller {
private IrisPapiInstaller() {
}
public static boolean install(PlaceholderRegistration registration, IrisPapiState state, Logger logger) {
return registration.register(() -> new IrisPapiExpansion(state, logger));
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import org.bukkit.Location;
import org.bukkit.World;
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.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.Objects;
import java.util.UUID;
public final class IrisPapiListener implements Listener {
private final IrisPapiState state;
public IrisPapiListener(IrisPapiState state) {
this.state = Objects.requireNonNull(state, "state");
}
static void track(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, false);
}
static void trackNow(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, true);
}
private static void publish(IrisPapiState state, UUID playerId, Location location, boolean immediate) {
if (state == null || playerId == null || location == null) {
return;
}
World world = location.getWorld();
if (world == null) {
return;
}
if (immediate) {
state.trackPositionNow(playerId, world, location.getBlockX(), location.getBlockZ());
return;
}
state.trackPosition(playerId, world, location.getBlockX(), location.getBlockZ());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
track(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerPortal(PlayerPortalEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getRespawnLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
if (player != null) {
state.release(player.getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPregeneration(IrisPregenerationEvent event) {
state.publishPregen(event.getPhase(), event.getProgress());
}
}
@@ -0,0 +1,9 @@
package art.arcane.iris.core.link;
import org.bukkit.World;
public record IrisPapiPosition(World world, int blockX, int blockZ, long publishedAtMs) {
public boolean sameColumn(World other, int otherBlockX, int otherBlockZ) {
return world == other && blockX == otherBlockX && blockZ == otherBlockZ;
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
public record IrisPapiPregenView(
String world,
String percent,
String eta,
String etaText,
String chunks,
String total,
String chunksPerSecond,
String paused) {
private static final long MILLIS_PER_SECOND = 1_000L;
private static final long SECONDS_PER_MINUTE = 60L;
private static final long SECONDS_PER_HOUR = 3_600L;
public static IrisPapiPregenView of(IrisPregenProgress progress) {
if (progress == null) {
return null;
}
return new IrisPapiPregenView(
PlaceholderValues.text(progress.worldName()),
PlaceholderValues.num(progress.percent()),
PlaceholderValues.count(progress.etaMillis() / MILLIS_PER_SECOND),
duration(progress.etaMillis()),
PlaceholderValues.count(progress.generatedChunks()),
PlaceholderValues.count(progress.totalChunks()),
PlaceholderValues.num(progress.chunksPerSecond()),
PlaceholderValues.bool(progress.paused()));
}
static String duration(long millis) {
long totalSeconds = millis / MILLIS_PER_SECOND;
long hours = totalSeconds / SECONDS_PER_HOUR;
long minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
long seconds = totalSeconds % SECONDS_PER_MINUTE;
if (hours > 0L) {
return hours + "h " + minutes + "m";
}
if (minutes > 0L) {
return minutes + "m " + seconds + "s";
}
return seconds + "s";
}
}
@@ -0,0 +1,204 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderSnapshot;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import art.arcane.volmlib.util.bukkit.papi.PlayerSnapshotStore;
import org.bukkit.World;
import java.util.Objects;
import java.util.UUID;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
public final class IrisPapiState {
static final long VIEW_TTL_MS = 1_000L;
static final long POSITION_INTERVAL_MS = 1_000L;
private final Supplier<IrisTerrainService> terrain;
private final LongSupplier clock;
private final PlayerSnapshotStore<IrisPapiPosition> positions = new PlayerSnapshotStore<>();
private final PlayerSnapshotStore<IrisPapiWorldView> views = new PlayerSnapshotStore<>();
private final PlaceholderSnapshot<IrisPapiPregenView> pregen = new PlaceholderSnapshot<>();
public IrisPapiState(Supplier<IrisTerrainService> terrain) {
this(terrain, System::currentTimeMillis);
}
public IrisPapiState(Supplier<IrisTerrainService> terrain, LongSupplier clock) {
this.terrain = Objects.requireNonNull(terrain, "terrain");
this.clock = Objects.requireNonNull(clock, "clock");
}
public void trackPosition(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, false);
}
public void trackPositionNow(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, true);
}
private void publishPosition(UUID playerId, World world, int blockX, int blockZ, boolean immediate) {
if (playerId == null || world == null) {
return;
}
IrisPapiPosition current = positions.get(playerId);
if (current != null && current.sameColumn(world, blockX, blockZ)) {
return;
}
long now = clock.getAsLong();
if (!immediate && current != null && current.world() == world
&& now - current.publishedAtMs() < POSITION_INTERVAL_MS) {
return;
}
positions.publish(playerId, new IrisPapiPosition(world, blockX, blockZ, now));
}
public void release(UUID playerId) {
positions.publish(playerId, null);
views.publish(playerId, null);
}
public void publishPregen(IrisPregenPhase phase, IrisPregenProgress progress) {
if (phase == null || progress == null) {
return;
}
pregen.publish(switch (phase) {
case STARTED, TICK, PAUSED, RESUMED, SAVING -> IrisPapiPregenView.of(progress);
case COMPLETED, CANCELLED -> null;
});
}
public void clear() {
positions.clear();
views.clear();
pregen.publish(null);
}
public String available(UUID playerId) {
return PlaceholderValues.bool(terrain.get() != null);
}
public String worldAvailable(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.FALSE : view.available();
}
public String biome(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biome();
}
public String biomeKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biomeKey();
}
public String region(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.region();
}
public String regionKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.regionKey();
}
public String dimension(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.dimension();
}
public String pregenAvailable(UUID playerId) {
return pregen.available();
}
public String pregenWorld(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.world();
}
public String pregenPercent(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.percent();
}
public String pregenEta(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.eta();
}
public String pregenEtaText(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.etaText();
}
public String pregenChunks(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunks();
}
public String pregenTotal(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.total();
}
public String pregenChunksPerSecond(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunksPerSecond();
}
public String pregenPaused(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.paused();
}
private IrisPapiWorldView viewOf(UUID playerId) {
IrisPapiPosition position = positions.get(playerId);
if (position == null) {
return null;
}
IrisPapiWorldView cached = views.get(playerId);
long now = clock.getAsLong();
if (cached != null && cached.position() == position && now - cached.builtAtMs() < VIEW_TTL_MS) {
return cached;
}
IrisPapiWorldView built = build(position, now);
views.publish(playerId, built);
return built;
}
private IrisPapiWorldView build(IrisPapiPosition position, long now) {
IrisTerrainService service = terrain.get();
World world = position.world();
if (service == null || !service.isIrisWorld(world)) {
return IrisPapiWorldView.absent(position, now);
}
int blockX = position.blockX();
int blockZ = position.blockZ();
return IrisPapiWorldView.present(
position,
now,
service.surfaceBiomeName(world, blockX, blockZ),
service.surfaceBiomeKey(world, blockX, blockZ),
service.regionName(world, blockX, blockZ),
service.regionKey(world, blockX, blockZ),
service.worldInfo(world).map(IrisWorldInfo::dimensionKey));
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import java.util.Optional;
public record IrisPapiWorldView(
IrisPapiPosition position,
long builtAtMs,
String available,
String biome,
String biomeKey,
String region,
String regionKey,
String dimension) {
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.FALSE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE);
}
public static IrisPapiWorldView present(
IrisPapiPosition position,
long builtAtMs,
Optional<String> biome,
Optional<String> biomeKey,
Optional<String> region,
Optional<String> regionKey,
Optional<String> dimension) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.TRUE,
text(biome),
text(biomeKey),
text(region),
text(regionKey),
text(dimension));
}
static String text(Optional<String> value) {
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
}
}
@@ -0,0 +1,208 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.runtime;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.object.IrisWorldBoundary;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.World;
import org.bukkit.WorldBorder;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public void refreshWorkspace(Engine engine) {
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
DatapackIngestService.refreshWorkspace(engine.getData());
}
@Override
public void reloadDatapacks(Engine engine) {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
}
@Override
public void fireHotloadEvent(Engine engine) {
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED);
}
@Override
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
IrisDimensionRuntimeContract.requireHotloadCompatible(
"Bukkit Studio world '" + engine.getWorld().name() + "'",
engine.getDimension(),
replacement,
"iris");
}
@Override
public void applyWorldBoundary(Engine engine) {
IrisDimension expectedDimension = engine.getDimension();
IrisWorldBoundary configuredBoundary = expectedDimension.getWorldBoundary();
if (configuredBoundary == null) {
return;
}
IrisWorld expectedIrisWorld = engine.getWorld();
World expectedWorld = BukkitWorldBinding.world(expectedIrisWorld);
if (expectedWorld == null) {
return;
}
IrisWorldBoundary boundary;
try {
boundary = IrisWorldBoundary.snapshot(configuredBoundary);
} catch (Throwable error) {
IrisLogging.error("Invalid Iris world boundary for " + expectedWorld.getName() + ".");
IrisLogging.reportError(error);
throw propagateBoundaryFailure("Invalid Iris world boundary for '" + expectedWorld.getName() + "'.", error);
}
if (!J.runGlobal(() -> applyWorldBoundary(engine, expectedDimension, expectedIrisWorld, expectedWorld, boundary))) {
IllegalStateException failure = new IllegalStateException(
"Bukkit global scheduler rejected world-boundary application for '" + expectedWorld.getName() + "'.");
IrisLogging.error(failure.getMessage());
IrisLogging.reportError(failure);
throw failure;
}
}
@Override
public boolean isPregeneratorActive(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
}
@Override
public void shutdownPregenerator(Engine engine) {
IrisWorld world = engine.getWorld();
if (world != null) {
PregeneratorJob.shutdownInstanceForWorld(world.identity());
}
}
@Override
public boolean shouldDisableChunkContextCache(Engine engine) {
IrisWorld world = engine.getWorld();
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
return false;
}
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
}
@Override
public boolean shouldSkipMantleCleanup(Engine engine) {
IrisWorld world = engine.getWorld();
return world != null
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
&& !isPregeneratorActive(engine);
}
@Override
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
IrisWorld irisWorld = engine.getWorld();
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(irisWorld);
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
}
@Override
public boolean shouldBypassMantleStages(Engine engine) {
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(engine.getWorld());
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
}
static void applyWorldBoundary(WorldBorder worldBorder, IrisWorldBoundary boundary) {
if (boundary == null) {
return;
}
worldBorder.setCenter(boundary.getCenter().getX(), boundary.getCenter().getZ());
worldBorder.setSize(boundary.getSize());
worldBorder.setWarningDistance(boundary.getWarningDistance());
worldBorder.setDamageBuffer(boundary.getDamageBuffer());
worldBorder.setDamageAmount(boundary.getDamageAmount());
}
private static void applyWorldBoundary(Engine engine, IrisDimension expectedDimension,
IrisWorld expectedIrisWorld, World expectedWorld,
IrisWorldBoundary boundary) {
if (engine.isClosed() || engine.isClosing() || engine.getDimension() != expectedDimension
|| engine.getWorld() != expectedIrisWorld
|| BukkitWorldBinding.world(expectedIrisWorld) != expectedWorld) {
return;
}
try {
applyWorldBoundary(expectedWorld.getWorldBorder(), boundary);
} catch (Throwable error) {
IrisLogging.error("Failed to apply Iris world boundary to " + expectedWorld.getName() + ".");
IrisLogging.reportError(error);
throw propagateBoundaryFailure("Failed to apply Iris world boundary to '"
+ expectedWorld.getName() + "'.", error);
}
}
private static RuntimeException propagateBoundaryFailure(String message, Throwable error) {
if (error instanceof Error fatal) {
throw fatal;
}
if (error instanceof RuntimeException runtimeException) {
return runtimeException;
}
return new IllegalStateException(message, error);
}
}
@@ -0,0 +1,333 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorSystem;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorEngineOptions;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
import art.arcane.volmlib.util.director.runtime.DirectorSender;
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.director.help.DirectorMiniMenu.DirectorHelpPage;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
private static final String ROOT_COMMAND = "iris";
private static final String ROOT_PERMISSION = "iris.all";
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient ThreadLocal<CommandSender> dispatchSenders = new ThreadLocal<>();
@Override
public void onEnable() {
PluginCommand command = findBukkitCommand();
if (command != null) {
command.setExecutor(this);
command.setTabCompleter(this);
} else {
registerPaperCommand();
}
J.a(this::getDirector);
}
@Override
public void onDisable() {
}
public DirectorRuntimeEngine getDirector() {
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
new CommandIris(),
DirectorEngineOptions.builder()
.contexts(buildDirectorContexts())
.dispatcher(this::dispatchDirector)
.invocationHook(this)
.legacyHandlers(DirectorSystem.handlers)
.textResolver(IrisLanguage.directorResolver())
.build()
));
}
private DirectorContextRegistry buildDirectorContexts() {
DirectorContextRegistry contexts = new DirectorContextRegistry();
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
registerContextHandler(contexts, entry.getKey(), entry.getValue());
}
return contexts;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
contexts.register((Class) type, (invocation, map) -> {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
}
return null;
});
}
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
if (mode != DirectorExecutionMode.SYNC) {
runnable.run();
return;
}
CommandSender sender = dispatchSenders.get();
if (sender instanceof Player player) {
if (!J.runEntity(player, runnable)) {
throw new IllegalStateException("Failed to schedule player command on its entity thread");
}
return;
}
J.s(runnable);
}
@Override
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
DirectorContext.touch(new VolmitSender(sender.sender()));
}
}
@Override
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
DirectorContext.remove();
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return List.of();
}
return tabCompleteRoot(sender, alias, args);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return false;
}
executeRoot(sender, label, args);
return true;
}
public void executeRoot(CommandSender sender, String label, String[] args) {
if (!sender.hasPermission(ROOT_PERMISSION)) {
ComponentMessenger.sendSection(sender, IrisLanguage.text(
IrisMessages.COMMAND_PERMISSION_DENIED,
MessageArgument.trusted("permission", ROOT_PERMISSION)
));
return;
}
J.aBukkit(() -> executeCommand(sender, label, args));
}
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
List<String> suggestions = runDirectorTab(sender, alias, args);
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
}
return suggestions;
}
private PluginCommand findBukkitCommand() {
try {
return Iris.instance.getCommand(ROOT_COMMAND);
} catch (UnsupportedOperationException ignored) {
return null;
}
}
private void registerPaperCommand() {
try {
Class<?> registrarType = Class.forName(
"art.arcane.iris.core.commands.PaperCommandRegistrar",
true,
getClass().getClassLoader()
);
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
register.invoke(null, Iris.instance, this);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new IllegalStateException("Paper command registration failed", cause);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Paper command registrar is unavailable", e);
} catch (LinkageError e) {
throw new IllegalStateException("Paper command APIs are unavailable", e);
}
}
private void executeCommand(CommandSender sender, String label, String[] args) {
if (sendHelpIfRequested(sender, args)) {
playSuccessSound(sender);
return;
}
DirectorExecutionResult result = runDirector(sender, label, args);
if (result.isSuccess()) {
playSuccessSound(sender);
return;
}
playFailureSound(sender);
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
new VolmitSender(sender).sendMessage(C.RED + IrisLanguage.text(IrisMessages.COMMAND_UNKNOWN));
}
}
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
DirectorMiniMenu.deliver(
sender,
request.get(),
DirectorMiniMenu.Theme.irisGreen(),
IrisLanguage.directorResolver()
);
return true;
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender);
try {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
} finally {
dispatchSenders.remove();
}
}
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
DirectorContext.touch(new VolmitSender(sender));
try {
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return List.of();
} finally {
DirectorContext.remove();
}
}
private void playFailureSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f,
Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
}
}
private void playSuccessSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f,
Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
}
}
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
try {
player.playSound(player.getLocation(), a, av, ap);
player.playSound(player.getLocation(), b, bv, bp);
} catch (Throwable e) {
if (SOUND_LOGGED.compareAndSet(false, true)) {
try {
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
} catch (Throwable ignored) {
}
}
}
}
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
@Override
public String getName() {
return sender.getName();
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
@Override
public void sendMessage(String message) {
if (message != null && !message.trim().isEmpty()) {
ComponentMessenger.sendLiteral(sender, message);
}
}
}
}
@@ -0,0 +1,121 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldInitEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.io.IOException;
import java.util.Set;
public final class DatapackStructureScopeSVC implements IrisService {
private DatapackStructureScopeIndex scopeIndex = DatapackStructureScopeIndex.create(null);
@Override
public void onEnable() {
try {
scopeIndex = DatapackStructureScopeIndex.create(
DatapackIngestService.installedStructureScopeResources());
} catch (IOException e) {
// A missing manifest entry is the condition validateOnStartup already classified as
// non-fatal; degrade to an empty scope and lock world creation instead of aborting
// the whole plugin bootstrap (which would skip listeners and journal reconciliation).
scopeIndex = DatapackStructureScopeIndex.create(null);
IrisLogging.error("Could not establish ownership for installed datapack structure sets; "
+ "datapack structure scoping is disabled until the manifest is repaired: " + e.getMessage());
IrisStartupValidation.markDatapacksInvalid(
"Iris could not establish ownership for installed datapack structure sets: " + e.getMessage());
}
for (World world : Bukkit.getWorlds()) {
try {
IrisImportedStructureControl importedStructures = importedStructures(world);
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
applyScope(world, importedStructures);
}
} catch (Throwable e) {
// Per-world containment: one world's scoping failure must not abort the
// service enable (and with it the whole bootstrap's containment contract).
IrisLogging.error("Could not scope datapack structures for world '" + world.getName() + "'.");
IrisLogging.reportError(e);
}
}
}
@Override
public void onDisable() {
for (World world : Bukkit.getWorlds()) {
INMS.get().abandonStudioStructureBootstrap(world);
}
scopeIndex = DatapackStructureScopeIndex.create(null);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onWorldInit(WorldInitEvent event) {
boolean studioEntryBootstrap = event.getWorld().getGenerator()
instanceof BukkitChunkGenerator generator
&& generator.isStudioEntryBootstrapActive();
IrisImportedStructureControl importedStructures = importedStructures(event.getWorld());
if (shouldApplyScope(scopeIndex.isEmpty(), studioEntryBootstrap,
importedStructures != null && importedStructures.hasFrequencyOverrides())) {
applyScope(event.getWorld(), importedStructures);
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onWorldUnload(WorldUnloadEvent event) {
INMS.get().abandonStudioStructureBootstrap(event.getWorld());
}
static boolean shouldApplyScope(boolean scopeIndexEmpty, boolean studioEntryBootstrap,
boolean hasFrequencyOverrides) {
return !scopeIndexEmpty || studioEntryBootstrap || hasFrequencyOverrides;
}
private void applyScope(World world, IrisImportedStructureControl importedStructures) {
Set<String> declaredSources = declaredSources(world);
IrisImportedStructureControl activeControl = importedStructures == null
? new IrisImportedStructureControl()
: importedStructures;
try {
DatapackStructureScopeResult result = INMS.get().scopeDatapackStructures(
world, scopeIndex, declaredSources, activeControl);
IrisLogging.info("Scoped Iris-managed datapack structure sets for world '"
+ world.getName() + "': " + result.retainedManagedSets() + " retained, "
+ result.excludedManagedSets() + " excluded.");
} catch (Throwable error) {
throw new IllegalStateException("Could not scope Iris-managed datapack structure sets for world '"
+ world.getName() + "'", error);
}
}
private IrisImportedStructureControl importedStructures(World world) {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator == null || generator.getTarget() == null
|| generator.getTarget().getDimension() == null) {
return null;
}
return generator.getTarget().getDimension().getImportedStructures();
}
private Set<String> declaredSources(World world) {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator == null) {
return Set.of();
}
return scopeIndex.declaredSources(
generator.getTarget().getDimension().getDatapackImports());
}
}
@@ -0,0 +1,126 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockEditor;
import art.arcane.iris.core.edit.BukkitBlockEditor;
import art.arcane.iris.engine.framework.BlockEditAccess;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldUnloadEvent;
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
private KMap<World, BlockEditor> editors;
private int updateTaskId = -1;
@Override
public void onEnable() {
this.editors = new KMap<>();
updateTaskId = J.sr(this::update, 1000);
}
@Override
public void onDisable() {
if (updateTaskId != -1) {
J.csr(updateTaskId);
updateTaskId = -1;
}
if (editors == null) {
return;
}
flushNow();
}
public BlockData get(World world, int x, int y, int z) {
return open(world).get(x, y, z);
}
public void set(World world, int x, int y, int z, BlockData d) {
open(world).set(x, y, z, d);
}
public void setBiome(World world, int x, int y, int z, Biome d) {
open(world).setBiome(x, y, z, d);
}
public void setBiome(World world, int x, int z, Biome d) {
open(world).setBiome(x, z, d);
}
public Biome getBiome(World world, int x, int y, int z) {
return open(world).getBiome(x, y, z);
}
public Biome getBiome(World world, int x, int z) {
return open(world).getBiome(x, z);
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (editors == null) {
return;
}
if (editors.containsKey(e.getWorld())) {
editors.remove(e.getWorld()).close();
}
}
public void update() {
if (editors == null) {
return;
}
for (World i : editors.k()) {
if (M.ms() - editors.get(i).last() > 1000) {
editors.remove(i).close();
}
}
}
public void flushNow() {
if (editors == null) {
return;
}
for (World i : editors.k()) {
editors.remove(i).close();
}
}
public BlockEditor open(World world) {
if (editors == null) {
editors = new KMap<>();
}
if (editors.containsKey(world)) {
return editors.get(world);
}
BlockEditor e = new BukkitBlockEditor(world);
editors.put(world, e);
return e;
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class FellingRun {
final TreeClaim claim;
final TreeCandidate candidate;
final int preservationChance;
final TreeFellerRunHooks runHooks;
final int heldSlot;
final TreeFellerPresentation presentation;
final AtomicBoolean finished = new AtomicBoolean();
final AtomicInteger cursor = new AtomicInteger();
final AtomicInteger processed = new AtomicInteger();
volatile int blocksPerPulse = 1;
volatile int effectStride = 1;
volatile ItemStack expectedTool;
volatile List<TreeMember> work = List.of();
volatile String abortReason;
FellingRun(
TreeClaim claim,
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks,
int heldSlot,
Location fallbackLocation
) {
this.claim = claim;
this.candidate = candidate;
this.preservationChance = preservationChance;
this.runHooks = runHooks;
this.heldSlot = heldSlot;
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
this.expectedTool = candidate.tool().clone();
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.api.world.IrisWorldEngineEvent;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenApiPhase;
import art.arcane.iris.core.pregenerator.PregenApiSink;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.Event;
public class IrisApiEventSVC implements IrisService, PregenApiSink {
public static void fireWorldPhase(World world, IrisWorldPhase phase) {
if (phase == null) {
return;
}
if (world == null) {
IrisLogging.debug("Iris world API skipped phase " + phase
+ " because the engine has no bound platform world.");
return;
}
try {
deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase)));
} catch (Throwable error) {
IrisLogging.reportError("Iris world API dispatch failed for phase " + phase
+ " on world \"" + world.getName() + "\".", error);
}
}
private static IrisWorldInfo describe(World world, IrisWorldPhase phase) {
try {
return IrisWorldInfoFactory.forWorld(world);
} catch (Throwable error) {
IrisLogging.reportError("Iris world API could not describe world \"" + world.getName()
+ "\" for phase " + phase + "; the event is delivered without world info.", error);
return null;
}
}
private static void deliver(Event event) {
if (Bukkit.isPrimaryThread()) {
Bukkit.getPluginManager().callEvent(event);
return;
}
Iris.callEvent(event);
}
private static IrisPregenPhase toApi(PregenApiPhase phase) {
return switch (phase) {
case STARTED -> IrisPregenPhase.STARTED;
case TICK -> IrisPregenPhase.TICK;
case PAUSED -> IrisPregenPhase.PAUSED;
case RESUMED -> IrisPregenPhase.RESUMED;
case SAVING -> IrisPregenPhase.SAVING;
case COMPLETED -> IrisPregenPhase.COMPLETED;
case CANCELLED -> IrisPregenPhase.CANCELLED;
};
}
private static IrisPregenProgress toApi(PregeneratorJob.PregenProgress progress) {
return new IrisPregenProgress(
progress.worldName(),
progress.worldIdentity(),
progress.percent(),
progress.generated(),
progress.totalChunks(),
progress.chunksRemaining(),
progress.failed(),
progress.chunksPerSecond(),
progress.eta(),
progress.elapsed(),
progress.method(),
progress.paused()
);
}
@Override
public void onEnable() {
IrisServices.register(PregenApiSink.class, this);
}
@Override
public void onDisable() {
IrisServices.remove(PregenApiSink.class);
}
@Override
public void pregen(PregenApiPhase phase, PregeneratorJob.PregenProgress progress) {
if (phase == null || progress == null || progress.worldIdentity() == null) {
return;
}
Iris.callEvent(new IrisPregenerationEvent(toApi(phase), toApi(progress)));
}
}
@@ -0,0 +1,800 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.lifecycle.WorldUnloadBoundaryRegistry;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class IrisEngineSVC implements IrisService {
private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L;
private static final long METRICS_PERIOD_MILLIS = 1_000L;
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
private final Object registrationLock = new Object();
private final Set<ClosingGenerator> closingGenerators = new HashSet<>();
private final AtomicReference<IrisEngineStatus.MaintenanceMetrics> metrics =
new AtomicReference<>(IrisEngineStatus.MaintenanceMetrics.EMPTY);
private final AtomicReference<IrisTelemetrySnapshot> telemetry =
new AtomicReference<>(IrisTelemetrySnapshot.EMPTY);
private final Map<World, CompletableFuture<Void>> pendingRegistrations = new HashMap<>();
private final Map<World, Registered> worlds = new ConcurrentHashMap<>();
private final IrisWorldPhaseLedger phases = new IrisWorldPhaseLedger();
private volatile ScheduledThreadPoolExecutor service;
private volatile ScheduledFuture<?> metricsTask;
@Override
public void onEnable() {
ScheduledThreadPoolExecutor current = service;
if (current != null && !current.isShutdown()) {
return;
}
IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC();
ThreadFactory threadFactory = (settings.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(settings.getPriority()))
.name("Iris EngineSVC-", 0)
.factory();
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
EngineMaintenance.workerParallelism(), threadFactory);
executor.setRemoveOnCancelPolicy(true);
executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
service = executor;
for (World world : Bukkit.getWorlds()) {
add(world);
}
metricsTask = executor.scheduleWithFixedDelay(
this::updateMetricsSafely,
0L,
METRICS_PERIOD_MILLIS,
TimeUnit.MILLISECONDS);
}
@Override
public void onDisable() {
ScheduledThreadPoolExecutor activeService = service;
service = null;
ScheduledFuture<?> activeMetricsTask = metricsTask;
metricsTask = null;
if (activeMetricsTask != null) {
activeMetricsTask.cancel(false);
}
List<Teardown> teardowns = new ArrayList<>();
List<CompletableFuture<Void>> generatorCloses;
synchronized (registrationLock) {
for (Map.Entry<World, Registered> entry : worlds.entrySet()) {
Registered registered = entry.getValue();
registered.close();
teardowns.add(new Teardown(entry.getKey(), registered, reserveClose(registered)));
}
worlds.clear();
pendingRegistrations.clear();
generatorCloses = new ArrayList<>(closingGenerators.size());
for (ClosingGenerator closing : closingGenerators) {
generatorCloses.add(closing.completion());
}
}
for (Teardown teardown : teardowns) {
phases.closing(teardown.world());
}
shutdownAndDrain(activeService);
for (Teardown teardown : teardowns) {
startClose(teardown.registered(), teardown.closing());
}
awaitGeneratorShutdown(generatorCloses);
resetMetrics();
}
public void engineStatus(VolmitSender sender) {
ScheduledThreadPoolExecutor activeService = service;
ScheduledFuture<?> activeMetricsTask = metricsTask;
boolean serviceRunning = activeService != null && !activeService.isShutdown();
boolean metricsRunning = activeMetricsTask != null
&& !activeMetricsTask.isDone()
&& !activeMetricsTask.isCancelled();
IrisEngineStatus.send(sender, new IrisEngineStatus.Snapshot(
serviceRunning,
metricsRunning,
MAINTENANCE_PERIOD_MILLIS,
serviceRunning ? activeService.getCorePoolSize() : 0,
TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()),
MantleHeapPressure.usedFraction(),
metrics.get()));
}
IrisTelemetrySnapshot telemetrySnapshot() {
return telemetry.get();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onWorldUnload(WorldUnloadEvent event) {
World world = event.getWorld();
CompletionStage<Boolean> unloadBoundary = WorldUnloadBoundaryRegistry.claim(
WorldIdentity.serialize(world));
if (remove(world, unloadBoundary) || unloadBoundary != null) {
return;
}
closeUntrackedGenerator(world);
}
/**
* Closes the engine of a world this service never registered.
* <p>
* Registration is skipped while a previous generator for the same identity is still closing and while
* the world is not yet the server's world for its UID, and an unload that lands in that window would
* otherwise leave a live engine bound to a dead world. Only an unload Iris did not drive is covered:
* an Iris-driven unload carries a boundary and closes the generator in its own phase. The close is
* CAS-idempotent, so a redundant call returns the in-flight completion.
*/
private void closeUntrackedGenerator(World world) {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator == null || generator.isClosing()) {
return;
}
IrisLogging.debug("EngineSVC: closing untracked generator for unloaded world " + world.getName());
CompletableFuture<Void> close = generator.closeAsync();
if (close == null) {
return;
}
close.whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to close untracked generator for " + world.getName(), cause);
}
});
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
add(event.getWorld());
}
private void add(World world) {
ScheduledThreadPoolExecutor activeService = service;
if (world == null || activeService == null || activeService.isShutdown() || !isCurrentWorld(world)) {
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) {
return;
}
RegistrationIdentity registrationIdentity = RegistrationIdentity.from(world);
Registered replaced = null;
ClosingGenerator replacementClose = null;
CompletableFuture<Void> retryAfter = null;
boolean registered = false;
try {
synchronized (registrationLock) {
if (service != activeService || activeService.isShutdown() || !isCurrentWorld(world)) {
return;
}
Registered current = worlds.get(world);
if (current != null && !current.isClosed() && current.uses(access)) {
pendingRegistrations.remove(world);
return;
}
if (current != null) {
worlds.remove(world, current);
current.close();
replaced = current;
replacementClose = reserveClose(current);
retryAfter = replacementClose.completion();
} else {
ClosingGenerator closing = findClosingGenerator(registrationIdentity);
if (closing != null) {
retryAfter = closing.completion();
} else if (!access.isClosing() && !hasRegisteredConflict(registrationIdentity)) {
Registration registration = new Registration(world.getName(), access, registrationIdentity);
worlds.put(world, new Registered(registration, activeService));
pendingRegistrations.remove(world);
registered = true;
}
}
}
} catch (RejectedExecutionException exception) {
if (!activeService.isShutdown()) {
reportFailure("Failed to register engine maintenance for " + world.getName(), exception);
}
}
if (registered) {
phases.ready(world);
}
if (replacementClose != null) {
phases.closing(world);
retryRegistrationAfterClose(world, retryAfter);
startClose(replaced, replacementClose);
return;
}
if (retryAfter != null) {
retryRegistrationAfterClose(world, retryAfter);
}
}
/**
* @return true when a registration was found and its close was reserved
*/
private boolean remove(World world, CompletionStage<Boolean> unloadBoundary) {
if (world == null) {
return false;
}
Registered registered;
ClosingGenerator closing = null;
synchronized (registrationLock) {
pendingRegistrations.remove(world);
registered = worlds.remove(world);
if (registered != null) {
registered.close();
closing = reserveClose(registered);
}
}
if (closing == null) {
return false;
}
phases.closing(world);
deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary);
return true;
}
private void deferCloseUntilWorldUnload(
World world,
Registered registered,
ClosingGenerator closing,
CompletionStage<Boolean> unloadBoundary
) {
if (unloadBoundary == null) {
J.sfut(() -> startClose(registered, closing), 1)
.whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to defer generator close for " + registered.name(), cause);
completeClose(closing, cause);
}
});
return;
}
unloadBoundary.whenComplete((unloaded, failure) -> {
if (failure == null && Boolean.TRUE.equals(unloaded)) {
startClose(registered, closing);
return;
}
abandonClose(world, closing);
});
}
private void abandonClose(World world, ClosingGenerator closing) {
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().complete(null);
J.sfut(() -> {
if (isCurrentWorld(world)) {
add(world);
}
}, 1).whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to restore generator maintenance for " + world.getName(), cause);
}
});
}
private ClosingGenerator reserveClose(Registered registered) {
ClosingGenerator closing = new ClosingGenerator(
registered.registrationIdentity(),
new CompletableFuture<>());
closingGenerators.add(closing);
return closing;
}
private void startClose(Registered registered, ClosingGenerator closing) {
try {
CompletableFuture<Void> close = registered.closeGenerator();
if (close == null) {
throw new IllegalStateException("Generator close returned no completion future for " + registered.name());
}
close.whenComplete((ignored, failure) -> completeClose(closing, failure));
} catch (Throwable exception) {
reportFailure("Failed to start generator close for " + registered.name(), exception);
completeClose(closing, exception);
}
}
private void completeClose(ClosingGenerator closing, Throwable failure) {
if (failure == null) {
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().complete(null);
} else {
// A failed close must still stop conflicting with future registrations,
// otherwise the world never regains its maintenance task after a reload.
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().completeExceptionally(failure);
}
}
private ClosingGenerator findClosingGenerator(RegistrationIdentity registrationIdentity) {
for (ClosingGenerator closing : closingGenerators) {
if (closing.registrationIdentity().conflictsWith(registrationIdentity)) {
return closing;
}
}
return null;
}
private boolean hasRegisteredConflict(RegistrationIdentity registrationIdentity) {
for (Registered registered : worlds.values()) {
if (!registered.isClosed() && registered.registrationIdentity().conflictsWith(registrationIdentity)) {
return true;
}
}
return false;
}
private void retryRegistrationAfterClose(World world, CompletableFuture<Void> completion) {
synchronized (registrationLock) {
if (service == null || pendingRegistrations.get(world) == completion) {
return;
}
pendingRegistrations.put(world, completion);
}
completion.whenComplete((ignored, failure) -> {
synchronized (registrationLock) {
if (pendingRegistrations.get(world) != completion) {
return;
}
pendingRegistrations.remove(world);
}
if (failure != null) {
return;
}
ScheduledThreadPoolExecutor activeService = service;
if (activeService == null || activeService.isShutdown()) {
return;
}
try {
J.s(() -> add(world), 1);
} catch (Throwable exception) {
if (service != null) {
reportFailure("Failed to retry engine maintenance registration for " + world.getName(), exception);
}
}
});
}
private static boolean isCurrentWorld(World world) {
return Bukkit.getWorld(world.getUID()) == world;
}
private void updateMetricsSafely() {
try {
updateMetrics();
} catch (Throwable exception) {
reportFailure("Failed to update engine maintenance metrics", exception);
}
}
private void updateMetrics() {
long now = System.currentTimeMillis();
List<EngineTelemetrySnapshot> worldSnapshots = new ArrayList<>(worlds.size());
int maintenanceTasks = 0;
int irisWorlds = 0;
for (Registered registered : worlds.values()) {
if (registered.isClosed()) {
continue;
}
irisWorlds++;
if (registered.maintenanceInFlight()) {
maintenanceTasks++;
}
try {
Engine engine = registered.getEngine();
if (engine == null) {
continue;
}
if (engine.isClosing() && !engine.isClosed()) {
continue;
}
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
registered.close();
continue;
}
double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now);
worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now));
registered.clearMetricsFailure();
} catch (Throwable exception) {
if (EngineMaintenance.isMantleClosed(exception)) {
if (registered.generatorClosing()) {
registered.close();
}
} else {
registered.reportMetricsFailure(exception);
}
}
}
EngineTelemetrySnapshot.Aggregate aggregate = EngineTelemetrySnapshot.aggregate(worldSnapshots);
metrics.set(new IrisEngineStatus.MaintenanceMetrics(
aggregate.mantleResidentPlates(),
aggregate.mantleQueuedPlates(),
aggregate.loadedChunks(),
irisWorlds,
maintenanceTasks,
aggregate.mantleIdleAverageMs(),
aggregate.mantleIdleMaxMs(),
aggregate.mantleIdleMinMs()));
ScheduledThreadPoolExecutor activeService = service;
int workers = activeService == null || activeService.isShutdown()
? 0
: activeService.getCorePoolSize();
int closingCount;
int pendingCount;
synchronized (registrationLock) {
closingCount = closingGenerators.size();
pendingCount = pendingRegistrations.size();
}
double heapUsage = MantleHeapPressure.usedFraction();
telemetry.set(new IrisTelemetrySnapshot(
now,
worldSnapshots,
aggregate,
maintenanceTasks,
workers,
closingCount,
pendingCount,
heapUsage,
MantleHeapPressure.reclaimUrgency(heapUsage),
summarizeCaches(),
pregeneratorSnapshot()));
}
private IrisTelemetrySnapshot.CacheSnapshot summarizeCaches() {
IrisTelemetrySnapshot.CacheBucket resource = IrisTelemetrySnapshot.CacheBucket.EMPTY;
IrisTelemetrySnapshot.CacheBucket stream2d = IrisTelemetrySnapshot.CacheBucket.EMPTY;
IrisTelemetrySnapshot.CacheBucket stream3d = IrisTelemetrySnapshot.CacheBucket.EMPTY;
IrisTelemetrySnapshot.CacheBucket other = IrisTelemetrySnapshot.CacheBucket.EMPTY;
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
for (MeteredCache cache : caches) {
IrisTelemetrySnapshot.CacheBucket sample = new IrisTelemetrySnapshot.CacheBucket(
1,
cache.getSize(),
cache.getMaxSize());
if (cache instanceof ResourceLoader<?>) {
resource = resource.plus(sample);
} else if (cache instanceof CachedStream2D<?> || cache instanceof CachedDoubleStream2D) {
stream2d = stream2d.plus(sample);
} else if (cache instanceof CachedStream3D<?>) {
stream3d = stream3d.plus(sample);
} else {
other = other.plus(sample);
}
}
IrisTelemetrySnapshot.CacheBucket total = resource.plus(stream2d).plus(stream3d).plus(other);
return new IrisTelemetrySnapshot.CacheSnapshot(total, resource, stream2d, stream3d, other);
}
private IrisTelemetrySnapshot.PregenSnapshot pregeneratorSnapshot() {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
return IrisTelemetrySnapshot.PregenSnapshot.INACTIVE;
}
return new IrisTelemetrySnapshot.PregenSnapshot(
true,
progress.worldIdentity(),
progress.worldName(),
progress.paused(),
progress.percent(),
progress.generated(),
progress.totalChunks(),
progress.chunksRemaining(),
progress.chunksPerSecond(),
progress.eta(),
progress.elapsed(),
progress.failed());
}
private void shutdownAndDrain(ScheduledThreadPoolExecutor activeService) {
if (activeService == null) {
return;
}
activeService.shutdown();
try {
if (!activeService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
activeService.shutdownNow();
}
} catch (InterruptedException exception) {
activeService.shutdownNow();
Thread.currentThread().interrupt();
reportFailure("Interrupted while draining engine maintenance", exception);
}
}
private void awaitGeneratorShutdown(List<CompletableFuture<Void>> closes) {
if (closes.isEmpty()) {
return;
}
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
reportFailure("Interrupted while closing Iris generators", exception);
} catch (ExecutionException exception) {
Throwable cause = exception.getCause() == null ? exception : exception.getCause();
reportFailure("Unexpected generator close aggregation failure", cause);
} catch (TimeoutException exception) {
reportFailure("Timed out while closing Iris generators", exception);
}
}
private void resetMetrics() {
metrics.set(IrisEngineStatus.MaintenanceMetrics.EMPTY);
telemetry.set(IrisTelemetrySnapshot.EMPTY);
}
private static void reportFailure(String message, Throwable exception) {
IrisLogging.reportError(exception);
IrisLogging.error("EngineSVC: " + message);
}
private final class Registered {
private final String name;
private final PlatformChunkGenerator access;
private final RegistrationIdentity registrationIdentity;
private final AtomicBoolean metricsFailureReported = new AtomicBoolean();
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
private volatile ScheduledFuture<?> maintenance;
private volatile boolean closed;
private long lastGeneratedSampleAtMs;
private int lastGeneratedCount;
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
name = registration.name();
access = registration.access();
registrationIdentity = registration.registrationIdentity();
long offset = ThreadLocalRandom.current().nextLong(MAINTENANCE_PERIOD_MILLIS);
maintenance = executor.scheduleWithFixedDelay(
this::maintain,
offset,
MAINTENANCE_PERIOD_MILLIS,
TimeUnit.MILLISECONDS);
}
private void maintain() {
if (closed) {
return;
}
CompletableFuture<Void> completion = new CompletableFuture<>();
if (!activeMaintenance.compareAndSet(null, completion)) {
return;
}
try {
if (closed) {
return;
}
Engine engine = getEngine();
if (engine == null) {
return;
}
if (!EngineMaintenance.isAvailable(engine)) {
return;
}
boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine);
if (pregeneratorTargetsWorld || !EngineMaintenance.shouldRun(engine)) {
return;
}
EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine);
if (outcome.unloadedTectonicPlates() > 0) {
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW
+ outcome.unloadedTectonicPlates() + " TectonicPlates in " + C.RED
+ Form.duration(outcome.unloadDurationMillis(), 2) + " for " + name);
}
} catch (Throwable exception) {
if (EngineMaintenance.isMantleClosed(exception)) {
if (generatorClosing()) {
close();
}
return;
}
reportFailure("Failed maintenance for " + name, exception);
} finally {
completion.complete(null);
activeMaintenance.compareAndSet(completion, null);
}
}
private boolean uses(PlatformChunkGenerator candidate) {
return access == candidate;
}
private String name() {
return name;
}
private RegistrationIdentity registrationIdentity() {
return registrationIdentity;
}
private boolean generatorClosing() {
return access.isClosing();
}
private boolean isClosed() {
return closed;
}
private boolean maintenanceInFlight() {
return activeMaintenance.get() != null;
}
private double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
int safeGeneratedCount = Math.max(0, generatedCount);
long previousAt = lastGeneratedSampleAtMs;
int previousCount = lastGeneratedCount;
lastGeneratedSampleAtMs = sampledAtMs;
lastGeneratedCount = safeGeneratedCount;
if (previousAt <= 0L || sampledAtMs <= previousAt) {
return 0D;
}
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
return generatedDelta * 1000D / (sampledAtMs - previousAt);
}
private void close() {
if (closed) {
return;
}
closed = true;
ScheduledFuture<?> activeMaintenance = maintenance;
maintenance = null;
if (activeMaintenance != null) {
activeMaintenance.cancel(false);
}
}
private CompletableFuture<Void> closeGenerator() {
CompletableFuture<Void> runningMaintenance = activeMaintenance.get();
if (runningMaintenance == null) {
return invokeGeneratorClose();
}
return runningMaintenance.thenCompose(ignored -> invokeGeneratorClose());
}
private CompletableFuture<Void> invokeGeneratorClose() {
try {
CompletableFuture<Void> future = access.closeAsync();
if (future == null) {
IllegalStateException exception = new IllegalStateException(
"Generator close returned no completion future for " + name);
reportFailure("Failed to close generator for " + name, exception);
return CompletableFuture.failedFuture(exception);
}
return future.whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to close generator for " + name, cause);
}
});
} catch (Throwable exception) {
reportFailure("Failed to close generator for " + name, exception);
return CompletableFuture.failedFuture(exception);
}
}
private Engine getEngine() {
return closed ? null : access.getEngine();
}
private void clearMetricsFailure() {
metricsFailureReported.set(false);
}
private void reportMetricsFailure(Throwable exception) {
if (metricsFailureReported.compareAndSet(false, true)) {
reportFailure("Failed to sample metrics for " + name, exception);
}
}
}
private record Registration(String name, PlatformChunkGenerator access,
RegistrationIdentity registrationIdentity) {
}
record RegistrationIdentity(String worldIdentity, Path worldFolder) {
RegistrationIdentity {
worldIdentity = Objects.requireNonNull(worldIdentity);
worldFolder = Objects.requireNonNull(worldFolder).toAbsolutePath().normalize();
}
private static RegistrationIdentity from(World world) {
return new RegistrationIdentity(
WorldIdentity.serialize(world),
world.getWorldFolder().toPath());
}
boolean conflictsWith(RegistrationIdentity candidate) {
return worldIdentity.equals(candidate.worldIdentity)
|| worldFolder.equals(candidate.worldFolder);
}
}
private record ClosingGenerator(RegistrationIdentity registrationIdentity,
CompletableFuture<Void> completion) {
}
private record Teardown(World world, Registered registered, ClosingGenerator closing) {
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
final class IrisEngineStatus {
private IrisEngineStatus() {
}
static void send(VolmitSender sender, Snapshot snapshot) {
CacheSummary caches = summarizeCaches();
MaintenanceMetrics metrics = snapshot.metrics();
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_STATUS));
sender.sendMessage(IrisLanguage.text(
BukkitCommandMessages.IRIS_ENGINE_STATUS_SERVICE,
MessageArgument.trusted("value", status(snapshot.serviceRunning()))
));
sender.sendMessage(IrisLanguage.text(
BukkitCommandMessages.IRIS_ENGINE_STATUS_METRICS,
MessageArgument.trusted("value", status(snapshot.metricsRunning()))
));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAINTENANCE_PERIOD, MessageArgument.untrusted("value", Form.duration(snapshot.maintenancePeriodMillis()))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_WORKER_PARALLELISM, MessageArgument.untrusted("value", snapshot.workerParallelism())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_ACTIVE_WORLD_TASKS, MessageArgument.untrusted("value", metrics.activeTasks())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_TECTONIC_PLATES));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CONFIGURED_RETENTION, MessageArgument.untrusted("value", Form.duration(snapshot.retentionMillis()))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_HEAP_USAGE, MessageArgument.untrusted("value", Form.pc(snapshot.heapUsage()))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESIDENT, MessageArgument.untrusted("value", metrics.residentTectonicPlates())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_QUEUED, MessageArgument.untrusted("value", metrics.queuedTectonicPlates())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_AVERAGE_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.averageIdleDuration(), 2))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAX_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.maxIdleDuration(), 2))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MIN_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.minIdleDuration(), 2))));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CACHES));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESOURCE, MessageArgument.untrusted("value", caches.sizes()[0]), MessageArgument.untrusted("value2", caches.counts()[0])));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_2D_STREAM, MessageArgument.untrusted("value", caches.sizes()[1]), MessageArgument.untrusted("value2", caches.counts()[1])));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_3D_STREAM, MessageArgument.untrusted("value", caches.sizes()[2]), MessageArgument.untrusted("value2", caches.counts()[2])));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER, MessageArgument.untrusted("value", caches.sizes()[3]), MessageArgument.untrusted("value2", caches.counts()[3])));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER_2));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_IRIS_WORLDS, MessageArgument.untrusted("value", metrics.worlds())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_LOADED_CHUNKS, MessageArgument.untrusted("value", metrics.loadedChunks())));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE_2));
}
private static String status(boolean running) {
return IrisLanguage.text(running ? RuntimeUiMessages.STATUS_RUNNING : RuntimeUiMessages.STATUS_STOPPED);
}
private static CacheSummary summarizeCaches() {
long[] sizes = new long[4];
long[] counts = new long[4];
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
for (MeteredCache cache : caches) {
int type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
counts[type]++;
}
return new CacheSummary(sizes, counts);
}
record Snapshot(boolean serviceRunning,
boolean metricsRunning,
long maintenancePeriodMillis,
int workerParallelism,
long retentionMillis,
double heapUsage,
MaintenanceMetrics metrics) {
}
record MaintenanceMetrics(long residentTectonicPlates,
long queuedTectonicPlates,
long loadedChunks,
int worlds,
int activeTasks,
double averageIdleDuration,
double maxIdleDuration,
double minIdleDuration) {
static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D);
}
private record CacheSummary(long[] sizes, long[] counts) {
}
}
@@ -0,0 +1,385 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1),
CURRENT_PROTOCOL
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"metric-groups",
"iris-engine-metrics",
"iris-world-metrics"
);
private static final Map<String, String> TIMING_KEYS = Map.ofEntries(
Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS),
Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS),
Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS),
Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS),
Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS),
Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS),
Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS),
Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS),
Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS),
Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS),
Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS),
Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS),
Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS),
Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS)
);
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
public IrisIntegrationService() {
this(IrisIntegrationService::currentTelemetry);
}
IrisIntegrationService(Supplier<IrisTelemetrySnapshot> telemetrySupplier) {
this.telemetrySupplier = telemetrySupplier == null
? () -> IrisTelemetrySnapshot.EMPTY
: telemetrySupplier;
}
@Override
public void onEnable() {
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
Iris.verbose("Integration provider registered for Iris");
}
@Override
public void onDisable() {
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
}
@Override
public String pluginId() {
return "iris";
}
@Override
public String pluginVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public Set<IntegrationProtocolVersion> supportedProtocols() {
return SUPPORTED_PROTOCOLS;
}
@Override
public Set<String> capabilities() {
return CAPABILITIES;
}
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
Set<IntegrationMetricDescriptor> descriptors = new LinkedHashSet<>();
for (String key : IntegrationMetricSchema.irisKeys()) {
descriptors.add(IntegrationMetricSchema.descriptor(key));
}
return Set.copyOf(descriptors);
}
@Override
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
long now = System.currentTimeMillis();
if (request == null) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"missing request",
now
);
}
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
SUPPORTED_PROTOCOLS,
request.supportedProtocols()
);
if (negotiated.isEmpty()) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"no-common-protocol",
now
);
}
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiated.get(),
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
now
);
}
@Override
public IntegrationHeartbeat heartbeat() {
return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: Set.copyOf(metricKeys);
IrisTelemetrySnapshot snapshot = safeSnapshot();
long sampledAtMs = snapshot.sampledAtMs() > 0L
? snapshot.sampledAtMs()
: System.currentTimeMillis();
if (snapshot.sampledAtMs() <= 0L) {
return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs);
}
Map<String, IntegrationMetricSample> available = globalSamples(snapshot);
Map<String, IntegrationMetricSample> selected = new LinkedHashMap<>(requested.size());
for (String key : requested) {
IntegrationMetricSample sample = available.get(key);
selected.put(key, sample == null
? unavailable(key, "unsupported-key", sampledAtMs)
: sample);
}
return Map.copyOf(selected);
}
@Override
public List<IntegrationMetricGroup> metricGroups() {
IrisTelemetrySnapshot snapshot = safeSnapshot();
if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) {
return List.of();
}
List<EngineTelemetrySnapshot> worlds = new ArrayList<>(snapshot.worlds());
worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity));
List<IntegrationMetricGroup> groups = new ArrayList<>(worlds.size());
for (EngineTelemetrySnapshot world : worlds) {
groups.add(new IntegrationMetricGroup(
"world",
world.worldIdentity(),
world.worldName(),
Map.of(
"plugin", "iris",
"dimension", world.dimensionKey()
),
worldSamples(world, snapshot.pregenerator())
));
}
return List.copyOf(groups);
}
private Map<String, IntegrationMetricSample> globalSamples(IrisTelemetrySnapshot snapshot) {
long sampledAtMs = snapshot.sampledAtMs();
EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs);
putCacheSamples(samples, snapshot.caches(), sampledAtMs);
putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs);
putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs);
return Map.copyOf(samples);
}
private Map<String, IntegrationMetricSample> worldSamples(
EngineTelemetrySnapshot world,
IrisTelemetrySnapshot.PregenSnapshot pregenerator
) {
long sampledAtMs = world.sampledAtMs();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs);
boolean targetsWorld = pregenerator.active()
&& world.worldIdentity().equals(pregenerator.worldIdentity());
putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs);
putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs);
samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys());
return Map.copyOf(samples);
}
private void putCacheSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheSnapshot caches,
long sampledAtMs
) {
putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs);
}
private void putCacheBucket(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheBucket bucket,
String countKey,
String entriesKey,
String capacityKey,
String usageKey,
long sampledAtMs
) {
put(samples, countKey, bucket.count(), sampledAtMs);
put(samples, entriesKey, bucket.entries(), sampledAtMs);
put(samples, capacityKey, bucket.capacity(), sampledAtMs);
put(samples, usageKey, bucket.usage(), sampledAtMs);
}
private void putPregenSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
long sampledAtMs
) {
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs);
if (!pregenerator.active()) {
for (String key : Set.of(
IntegrationMetricSchema.IRIS_PREGEN_PAUSED,
IntegrationMetricSchema.IRIS_PREGEN_PROGRESS,
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
IntegrationMetricSchema.IRIS_PREGEN_FAILED)) {
samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs));
}
return;
}
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), 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_FAILED, pregenerator.failed(), sampledAtMs);
}
private void putTimingSamples(
Map<String, IntegrationMetricSample> samples,
Map<String, Double> timings,
long sampledAtMs
) {
for (Map.Entry<String, String> entry : TIMING_KEYS.entrySet()) {
Double value = timings.get(entry.getKey());
samples.put(entry.getValue(), value == null
? unavailable(entry.getValue(), "timing-not-available", sampledAtMs)
: available(entry.getValue(), value, sampledAtMs));
}
}
private void put(Map<String, IntegrationMetricSample> samples, String key, double value, long sampledAtMs) {
samples.put(key, available(key, value, sampledAtMs));
}
private IntegrationMetricSample available(String key, double value, long sampledAtMs) {
return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs);
}
private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) {
return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs);
}
private Map<String, IntegrationMetricSample> unavailableMetrics(
Set<String> keys,
String reason,
long sampledAtMs
) {
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>(keys.size());
for (String key : keys) {
samples.put(key, unavailable(key, reason, sampledAtMs));
}
return Map.copyOf(samples);
}
private IrisTelemetrySnapshot safeSnapshot() {
IrisTelemetrySnapshot snapshot = telemetrySupplier.get();
return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot;
}
private static IrisTelemetrySnapshot currentTelemetry() {
IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class);
return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot();
}
}
@@ -0,0 +1,237 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisCursorRequestService;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisServerTransport;
import art.arcane.iris.core.protocol.IrisSession;
import art.arcane.iris.core.protocol.IrisSessionRegistry;
import art.arcane.iris.core.protocol.IrisVisionRequestService;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.messaging.Messenger;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.util.UUID;
public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport {
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN
| IrisProtocol.CAPABILITY_VISION
| IrisProtocol.CAPABILITY_CURSOR
| IrisProtocol.CAPABILITY_STUDIO;
private static final int DIMENSION_SYNC_RETRY_TICKS = 20;
private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15;
private IrisSessionRegistry registry;
private IrisProtocolServer protocolServer;
private IrisCursorRequestService cursorService;
private IrisVisionRequestService visionService;
@Override
public void onEnable() {
Messenger messenger = Bukkit.getMessenger();
messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
registry = new IrisSessionRegistry();
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
protocolServer.setEngineResolver(engineResolver);
cursorService = IrisCursorRequestService.create(engineResolver, registry);
protocolServer.setCursorInfoHandler(cursorService);
visionService = IrisVisionRequestService.create(engineResolver, registry);
protocolServer.setVisionTileHandler(visionService);
IrisServices.register(IrisProtocolServer.class, protocolServer);
for (Player player : Bukkit.getOnlinePlayers()) {
registry.register(new IrisSession(player.getUniqueId().toString(), this));
}
}
@Override
public void onDisable() {
IrisServices.remove(IrisProtocolServer.class);
Messenger messenger = Bukkit.getMessenger();
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
IrisSessionRegistry current = registry;
IrisCursorRequestService cursor = cursorService;
IrisVisionRequestService vision = visionService;
if (current != null) {
for (IrisSession session : current.all()) {
current.unregister(session.id());
if (cursor != null) {
cursor.clearSession(session.id());
}
if (vision != null) {
vision.clearSession(session.id());
}
}
}
registry = null;
protocolServer = null;
cursorService = null;
visionService = null;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this));
syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS);
}
@EventHandler
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
if (registry == null) {
return;
}
syncDimension(event.getPlayer(), 1);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
String sessionId = event.getPlayer().getUniqueId().toString();
current.unregister(sessionId);
IrisCursorRequestService cursor = cursorService;
if (cursor != null) {
cursor.clearSession(sessionId);
}
IrisVisionRequestService vision = visionService;
if (vision != null) {
vision.clearSession(sessionId);
}
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
IrisProtocolServer current = protocolServer;
if (current == null || !IrisProtocol.CHANNEL.equals(channel)) {
return;
}
current.onClientFrame(player.getUniqueId().toString(), message);
}
@Override
public void sendToClient(String sessionId, byte[] frame) {
if (sessionId == null || frame == null) {
return;
}
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return;
}
J.runEntity(player, () -> deliver(player, frame));
}
private void deliver(Player player, byte[] frame) {
if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) {
return;
}
player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame);
}
private void syncDimension(Player player, int attemptsLeft) {
IrisProtocolServer protocol = protocolServer;
IrisSessionRegistry current = registry;
if (protocol == null || current == null || !player.isOnline()) {
return;
}
String sessionId = player.getUniqueId().toString();
IrisSession session = current.get(sessionId);
if (session == null) {
return;
}
if (!session.isReady()) {
if (attemptsLeft > 1) {
J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS);
}
return;
}
World world = player.getWorld();
Engine engine = engineFor(world);
if (engine != null) {
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
return;
}
protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false);
}
private static Engine resolveEngine(String sessionId) {
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return null;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return null;
}
return engineFor(player.getWorld());
}
private static Engine engineFor(World world) {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) {
return null;
}
Engine engine = access.getEngine();
if (engine == null || engine.isClosed()) {
return null;
}
return engine;
}
private static UUID parseUuid(String sessionId) {
try {
return UUID.fromString(sessionId);
} catch (IllegalArgumentException invalid) {
return null;
}
}
private static String brand() {
return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)";
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import java.util.List;
record IrisTelemetrySnapshot(
long sampledAtMs,
List<EngineTelemetrySnapshot> worlds,
EngineTelemetrySnapshot.Aggregate aggregate,
int maintenanceActiveTasks,
int maintenanceWorkers,
int closingEngines,
int pendingRegistrations,
double heapUsage,
double reclaimUrgency,
CacheSnapshot caches,
PregenSnapshot pregenerator
) {
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
0L,
List.of(),
EngineTelemetrySnapshot.aggregate(List.of()),
0,
0,
0,
0,
0D,
0D,
CacheSnapshot.EMPTY,
PregenSnapshot.INACTIVE
);
IrisTelemetrySnapshot {
worlds = worlds == null ? List.of() : List.copyOf(worlds);
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
maintenanceWorkers = Math.max(0, maintenanceWorkers);
closingEngines = Math.max(0, closingEngines);
pendingRegistrations = Math.max(0, pendingRegistrations);
heapUsage = finiteRatio(heapUsage);
reclaimUrgency = finiteRatio(reclaimUrgency);
caches = caches == null ? CacheSnapshot.EMPTY : caches;
pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator;
}
private static double finiteRatio(double value) {
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
}
record CacheSnapshot(
CacheBucket total,
CacheBucket resource,
CacheBucket stream2d,
CacheBucket stream3d,
CacheBucket other
) {
static final CacheSnapshot EMPTY = new CacheSnapshot(
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY
);
CacheSnapshot {
total = total == null ? CacheBucket.EMPTY : total;
resource = resource == null ? CacheBucket.EMPTY : resource;
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d;
other = other == null ? CacheBucket.EMPTY : other;
}
}
record CacheBucket(int count, long entries, long capacity) {
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
CacheBucket {
count = Math.max(0, count);
entries = Math.max(0L, entries);
capacity = Math.max(0L, capacity);
}
double usage() {
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
}
CacheBucket plus(CacheBucket other) {
if (other == null) {
return this;
}
return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity);
}
}
record PregenSnapshot(
boolean active,
String worldIdentity,
String worldName,
boolean paused,
double progressPercent,
long generated,
long total,
long remaining,
double chunksPerSecond,
long etaMs,
long elapsedMs,
long failed
) {
static final PregenSnapshot INACTIVE = new PregenSnapshot(
false,
"",
"",
false,
0D,
0L,
0L,
0L,
0D,
0L,
0L,
0L
);
PregenSnapshot {
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
worldName = worldName == null ? "" : worldName.trim();
progressPercent = Double.isFinite(progressPercent)
? Math.max(0D, Math.min(100D, progressPercent))
: 0D;
generated = Math.max(0L, generated);
total = Math.max(0L, total);
remaining = Math.max(0L, remaining);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMs = Math.max(0L, etaMs);
elapsedMs = Math.max(0L, elapsedMs);
failed = Math.max(0L, failed);
}
}
}
@@ -0,0 +1,316 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.terrain.IrisApiFaultGuard;
import art.arcane.iris.core.service.terrain.IrisColumnWalk;
import art.arcane.iris.core.service.terrain.IrisSampleLimits;
import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.ServicePriority;
import java.util.EnumSet;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicBoolean;
public class IrisTerrainSVC implements IrisService, IrisTerrainService {
private static final long FAULT_REPORT_INTERVAL_MILLIS = 60_000L;
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
private final IrisApiFaultGuard queryFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
private final IrisApiFaultGuard sinkFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
@Override
public void onEnable() {
serviceEnabled.set(true);
Bukkit.getServicesManager().register(
IrisTerrainService.class,
this,
BukkitPlatform.plugin(),
ServicePriority.Normal
);
IrisServices.register(IrisTerrainService.class, this);
}
@Override
public void onDisable() {
serviceEnabled.set(false);
Bukkit.getServicesManager().unregister(IrisTerrainService.class, this);
IrisServices.remove(IrisTerrainService.class);
}
@Override
public boolean isIrisWorld(World world) {
return generatorOf(world) != null;
}
@Override
public Optional<IrisWorldInfo> worldInfo(World world) {
PlatformChunkGenerator generator = liveGeneratorOf(world);
if (generator == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(IrisWorldInfoFactory.from(generator));
} catch (Throwable error) {
reportQueryFault("worldInfo", world, error);
return Optional.empty();
}
}
@Override
public OptionalInt surfaceHeight(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return OptionalInt.empty();
}
try {
return OptionalInt.of(engine.getHeight(blockX, blockZ) + engine.getMinHeight());
} catch (Throwable error) {
reportQueryFault("surfaceHeight", world, error);
return OptionalInt.empty();
}
}
@Override
public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return IrisSurfaceKind.UNKNOWN;
}
try {
int surface = engine.getHeight(blockX, blockZ);
int fluid = engine.getDimension().getFluidHeight();
InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType();
}
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
} catch (Throwable error) {
reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN;
}
}
@Override
public Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> surfaceBiomeName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return name(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeName", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getBiome(blockX, blockY - engine.getMinHeight(), blockZ));
} catch (Throwable error) {
reportQueryFault("biomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String loadKey = region == null ? null : region.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
} catch (Throwable error) {
reportQueryFault("regionKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String name = region == null ? null : region.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
} catch (Throwable error) {
reportQueryFault("regionName", world, error);
return Optional.empty();
}
}
@Override
public int maxSampleColumns() {
return IrisSampleLimits.maxColumns(noiseCacheChunks());
}
@Override
public int maxSampleChunks() {
return IrisSampleLimits.maxChunks(noiseCacheChunks());
}
@Override
public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) {
if (query == null || sink == null) {
return false;
}
Engine engine = liveEngineOf(world);
if (engine == null) {
return false;
}
int noiseCacheChunks = noiseCacheChunks();
if (!IrisSampleLimits.withinLimits(
query,
IrisSampleLimits.maxColumns(noiseCacheChunks),
IrisSampleLimits.maxChunks(noiseCacheChunks))) {
return false;
}
EnumSet<IrisColumnField> fields = query.fields();
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
try {
int minHeight = engine.getMinHeight();
int fluid = engine.getDimension().getFluidHeight();
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
if (engine.isClosed()) {
return false;
}
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
IrisSurfaceKind kind = wantKind
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
: IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
return true;
});
return visited == query.columnCount();
} catch (Throwable error) {
reportSinkFault(world, error);
return false;
}
}
private static Optional<String> key(IrisBiome biome) {
String loadKey = biome == null ? null : biome.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
}
private static Optional<String> name(IrisBiome biome) {
String name = biome == null ? null : biome.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
}
private static int noiseCacheChunks() {
return IrisSettings.get().getPerformance().getNoiseCacheSize();
}
static boolean answerable(boolean serviceEnabled, boolean worldPresent) {
return serviceEnabled && worldPresent;
}
private PlatformChunkGenerator generatorOf(World world) {
if (!answerable(serviceEnabled.get(), world != null)) {
return null;
}
ChunkGenerator generator = world.getGenerator();
return generator instanceof PlatformChunkGenerator platform ? platform : null;
}
private PlatformChunkGenerator liveGeneratorOf(World world) {
PlatformChunkGenerator generator = generatorOf(world);
return generator == null || generator.isClosing() ? null : generator;
}
private static Engine engineOf(PlatformChunkGenerator generator) {
if (generator == null) {
return null;
}
Engine engine = generator.getEngine();
return engine == null || engine.isClosed() ? null : engine;
}
private Engine liveEngineOf(World world) {
return engineOf(liveGeneratorOf(world));
}
private void reportQueryFault(String operation, World world, Throwable error) {
if (queryFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API query \"" + operation + "\" failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + queryFaults.faults()
+ " terrain API query faults so far).", error);
}
}
private void reportSinkFault(World world, Throwable error) {
if (sinkFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API column sample failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + sinkFaults.faults()
+ " terrain API sample faults so far). A third-party sink that throws is treated as a refusal.", error);
}
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
final class IrisWorldPhaseLedger {
@FunctionalInterface
interface Dispatch {
void fire(World world, IrisWorldPhase phase);
}
private final Set<UUID> announced = ConcurrentHashMap.newKeySet();
private final Dispatch dispatch;
IrisWorldPhaseLedger() {
this(IrisApiEventSVC::fireWorldPhase);
}
IrisWorldPhaseLedger(Dispatch dispatch) {
this.dispatch = Objects.requireNonNull(dispatch, "dispatch");
}
void ready(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.add(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_READY);
}
void closing(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.remove(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_CLOSING);
}
private static UUID identityOf(World world) {
return world == null ? null : world.getUID();
}
}

Some files were not shown because too many files have changed in this diff Show More