Compare commits

...

371 Commits

Author SHA1 Message Date
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
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
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 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 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 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 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 c35c858eee add scripts for engine setup 2025-07-07 21:06:36 +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 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 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
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 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
1691 changed files with 123422 additions and 99459 deletions
+31
View File
@@ -0,0 +1,31 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (core, plugin, spi, probes)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run verification gates
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
+30 -1
View File
@@ -10,4 +10,33 @@ libs/
collection/
/core/src/main/java/com/volmit/iris/util/uniques/
out/
tools/simd-bench/out/
tools/simd-bench/simd-bench.jar
DataPackExamples/
TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
adapters/*/run/
.codegraph/
plans/
.env
.env.*
!.env.example
local.properties
*.jks
*.p12
*.pfx
*.pem
*.key
*.hprof
credentials.json
service-account*.json
docs/
+115 -54
View File
@@ -1,75 +1,136 @@
# Iris
The master branch is for the latest version of minecraft.
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
or NeoForge server mod, and produces bit-identical terrain on every platform for the same pack and
seed. The master branch targets the current Minecraft version (26.2).
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
# Building
Consider supporting development by buying Iris on Spigot.
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.
## Platforms
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.2 | Full feature set |
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.0+ |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2+ |
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
Java 25 is required on every platform.
### Command Line Builds
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
workspaces with schema autocomplete over the server's live registries, entity spawning parity
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
the goldenhash determinism gate, which is interchangeable across all four platforms.
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 21
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
3. Once the project has setup, run `gradlew iris`
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
## Install
### IDE Builds (for development)
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
the server. On first boot Iris downloads the default `overworld` pack automatically.
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
prefer.
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris
downloads the default `overworld` pack before the worldgen datapack is written, so the default
pack is fully active immediately. Packs installed later register their custom dimension types
(height ranges) and custom biomes through the forced datapack at server start — restart once after
adding a pack so worlds get its full heights and biomes; worlds created before that restart run
with fallback heights.
# Iris Toolbelt
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
Create New World screen; the integrated server runs the same engine.
Everyone needs a tool-belt.
## The client mod
```java
package com.volmit.iris.core.tools;
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
// Get IrisDataManager from a world
IrisToolbelt.access(anyWorld).getCompound().getData();
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
mod is inert.
// Get Default Engine from world
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
## Quickstart
// Get the engine at the given height
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
Create and enter an Iris world.
// IS THIS THING ON?
boolean yes=IrisToolbelt.isIrisWorld(world);
Plugin (optional arguments are keyed):
// GTFO for worlds (moves players to any other world, just not this one)
IrisToolbelt.evacuate(world);
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
.name("myWorld") // The world name
.dimension("terrifyinghands")
.seed(69133742) // The world seed
.pregen(PregenTask // Define a pregen job to run
.builder()
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
.build())
.create();
```
/iris create myworld type=overworld seed=1337
/iris load myworld
```
Mod (positional arguments):
```
/iris create myworld overworld 1337
```
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
servers they are positional and composable:
```
/iris pregen start 352 world=myworld center=0,0 gui=false
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
```
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
console/status output. `/iris pregen status` reports progress on the plugin.
## Studio and VSCode workspace
The studio is the pack authoring environment, available on all platforms. Studio worlds are
transient — they are deleted on close and purged at startup.
```
/iris studio create <name> [template] scaffold a new pack (default template: example)
/iris studio open <pack> [seed] open a temporary studio world for live editing
/iris studio vscode [pack] write a .code-workspace with JSON schemas
/iris studio update [pack] regenerate the workspace schemas
/iris studio close close and discard the studio world
```
As in the quickstart, optional arguments are keyed on the plugin (`seed=1234`) and positional on
the mod.
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
files hotloads the changes and regenerates the schemas.
## Building from source
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```
builds every platform artifact into `dist/`:
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
Discord asking for free copies or a compile tutorial.
## Maintainer docs
- [Minecraft version bump checklist](docs/mc-version-bump.md)
- [Release checklist](docs/release-checklist.md)
@@ -0,0 +1,438 @@
package art.arcane.iris.core.nms.v26_2_R1;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
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.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.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.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
private static final int NOISE_BIOME_CACHE_MAX = 262144;
private final long seed;
private final Engine engine;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final KMap<String, Holder<Biome>> customBiomes;
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
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);
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
if (fallback != null) {
biomes.add(fallback);
}
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) {
biomes.add(vanillaHolder);
} else if (!i.isCustom() && fallback != null) {
biomes.add(fallback);
}
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) {
biomes.add(customHolder);
} else if (fallback != null) {
biomes.add(fallback);
}
}
}
}
return new ArrayList<>(biomes);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
if (o != null) {
return o;
}
return invokeFor(type, source);
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
}
private static Object invokeFor(Class<?> returns, Object in) {
for (Method i : in.getClass().getMethods()) {
if (i.getReturnType().equals(returns)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
for (Field i : sourceType.getDeclaredFields()) {
if (i.getType().equals(returnType)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return getAllBiomes(
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null),
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
engine,
fallbackBiome).stream();
}
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;
}
return vanillaSpawnBiomes.get(biome.value());
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
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;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
if (holder != null) {
return holder;
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
return getFallbackBiome();
}
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
org.bukkit.block.Biome vanillaBiome = resolution.underground
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
if (holder != null) {
return holder;
}
return getFallbackBiome();
}
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
if (holder != null) {
return holder;
}
}
return getFallbackBiome();
}
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
if (engine == null || engine.isClosed()) {
return null;
}
if (engine.getComplex() == null) {
return null;
}
int blockX = x << 2;
int blockZ = z << 2;
int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean belowSurface = internalY <= surfaceInternalY - 8;
boolean underground = deepUnderground && belowSurface;
IrisBiome irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null) {
return null;
}
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
}
private Holder<Biome> getFallbackBiome() {
if (fallbackBiome != null) {
return fallbackBiome;
}
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
if (holder != null) {
return holder;
}
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static 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> 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,478 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.Weighted;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.core.registries.Registries;
import java.util.stream.Collectors;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
import org.spigotmc.SpigotWorldConfig;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
private final Engine engine;
private final CustomBiomeSource customBiomeSource;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile Set<String> reachableStructureKeysCache;
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
}
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator());
this.delegate = delegate;
this.engine = engine;
this.customBiomeSource = customBiomeSource;
}
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
continue;
}
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (at == null) {
continue;
}
long dx = (long) at[0] - pos.getX();
long dz = (long) at[2] - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(at[0], at[1], at[2]);
bestHolder = holder;
}
}
if (best != null) {
return Pair.of(best, bestHolder);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (!importedControl().active()) {
return null;
}
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
if (reachable == null || reachable.size() == 0) {
return null;
}
try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
IrisLogging.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e);
IrisLogging.reportError(e);
return null;
}
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeysCache;
if (reachable == null) {
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
reachableStructureKeysCache = reachable;
}
if (reachable.isEmpty()) {
return holders;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>();
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id != null && reachable.contains(id.toString())) {
kept.add(holder);
}
}
if (kept.size() == holders.size()) {
return holders;
}
return HolderSet.direct(kept);
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(null);
}
@Override
public ChunkGenerator getDelegate() {
if (delegate instanceof CustomChunkGenerator chunkGenerator)
return chunkGenerator.getDelegate();
return delegate;
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
}
private IrisImportedStructureControl importedControl() {
return engine.getDimension().getImportedStructures();
}
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
return delegate.createState(holderlookup, randomstate, i, conf);
}
@Override
public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess);
}
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
}
@Override
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
}
@Override
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
}
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
}
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype);
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = delegate.getMobsAt(
vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition);
if (resolvedSpawns != vanillaSpawns) {
return resolvedSpawns;
}
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype);
if (explicitSpawns.isEmpty()) {
return vanillaSpawns;
}
if (vanillaSpawns.isEmpty()) {
return explicitSpawns;
}
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
}
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) {
applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true);
}
@Override
public void addDebugScreenInfo(List<String> list, RandomState randomstate, BlockPos blockposition) {
delegate.addDebugScreenInfo(list, randomstate, blockposition);
}
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
if (importedControl().active()) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
}
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.getOrDefault(step, List.of())) {
Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
random.setFeatureSeed(decoSeed, index, step);
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step()));
boolean shifted = offset[0] != 0 || offset[1] != 0 || offset[2] != 0;
WorldGenLevel target = shifted ? shiftedLevel(world, offset[0], offset[1], offset[2]) : world;
BoundingBox placeArea = shifted
? new BoundingBox(area.minX() - offset[0], area.minY(), area.minZ() - offset[2], area.maxX() - offset[0], area.maxY(), area.maxZ() - offset[2])
: area;
try {
structureManager.startsForStructure(sectionPos, structure)
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
index++;
}
}
}
private static boolean isUndergroundStep(GenerationStep.Decoration step) {
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
private WorldGenLevel shiftedLevel(WorldGenLevel world, int dx, int dy, int dz) {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class},
(proxy, method, args) -> {
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof BlockPos bp) {
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz);
}
}
}
try {
return method.invoke(world, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
});
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos cp = chunk.getPos();
int i = cp.getMinBlockX();
int j = cp.getMinBlockZ();
int minY = getMinY() + 1;
int maxY = getMinY() + engine.getHeight() - 1;
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
}
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int wX = x + blockPos.getX();
int wZ = z + blockPos.getZ();
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
SET_HEIGHT.invoke(surface, x, z, terrainTop);
SET_HEIGHT.invoke(motion, x, z, terrainTop);
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
}
}
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
}
@Override
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
delegate.spawnOriginalMobs(regionlimitedworldaccess);
}
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
Set<EntityType<?>> explicitTypes = new HashSet<>();
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
explicitTypes.add(entry.value().type());
}
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
if (!explicitTypes.contains(entry.value().type())) {
entries.add(entry);
}
}
entries.addAll(explicitSpawns.unwrap());
return WeightedList.of(entries);
}
@Override
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
return delegate.getSpawnHeight(levelheightaccessor);
}
@Override
public int getGenDepth() {
return delegate.getGenDepth();
}
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
}
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
else column[k] = Blocks.AIR.defaultBlockState();
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
}
@Override
public Optional<Identifier> getTypeNameForDataFixer() {
return delegate.getTypeNameForDataFixer();
}
@Override
public void validate() {
delegate.validate();
}
static {
Field biomeSource = null;
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
if (!field.getType().equals(BiomeSource.class))
continue;
biomeSource = field;
break;
}
if (biomeSource == null)
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
Method setHeight = null;
for (Method method : Heightmap.class.getDeclaredMethods()) {
var types = method.getParameterTypes();
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class))
continue;
setHeight = method;
break;
}
if (setHeight == null)
throw new RuntimeException("Could not find setHeight method in Heightmap!");
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
}
private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) {
try {
BIOME_SOURCE.set(generator, source);
if (generator instanceof CustomChunkGenerator custom)
BIOME_SOURCE.set(custom.getDelegate(), source);
return generator;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class VanillaStructureBiomes {
private VanillaStructureBiomes() {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
}
for (Holder<Biome> holder : source.possibleBiomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
Set<String> keys = new LinkedHashSet<>();
if (access == null || structureKey == null || structureKey.isEmpty()) {
return keys;
}
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
return keys;
}
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
Set<String> reachable = new LinkedHashSet<>();
if (level == null || source == null) {
return reachable;
}
Set<String> possible = possibleBiomeKeys(source);
if (possible.isEmpty()) {
return reachable;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
for (Holder<Biome> holder : entry.getValue().biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
reachable.add(entry.getKey().identifier().toString());
break;
}
}
}
return reachable;
}
}
+43
View File
@@ -0,0 +1,43 @@
String apiVersion = providers.gradleProperty('minecraftVersion').get()
def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')
.get()
dependencies {
compileOnly(project(':core'))
compileOnly(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.paper.api)
testImplementation('junit:junit:4.13.2')
testImplementation(libs.paper.api)
testImplementation(libs.bstats)
testImplementation(libs.sentry)
testImplementation(project(':core'))
testImplementation(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.placeholderApi)
compileOnly(libs.multiverseCore)
}
tasks.named('processResources').configure {
def pluginProperties = [
name : rootProject.name,
version : rootProject.version,
apiVersion: apiVersion,
main : mainClass,
bootstrapper: bootstrapperClass,
]
inputs.properties(pluginProperties)
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
expand(pluginProperties)
}
}
tasks.named('jar', Jar).configure {
archiveBaseName.set('iris-bukkit-plugin')
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
package art.arcane.iris;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import io.papermc.paper.datapack.Datapack;
import io.papermc.paper.datapack.DatapackRegistrar;
import io.papermc.paper.datapack.DiscoveredDatapack;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import java.io.IOException;
import java.nio.file.Path;
@SuppressWarnings("UnstableApiUsage")
public final class IrisBootstrap implements PluginBootstrap {
static final String PACK_ID = "generated";
@Override
public void bootstrap(BootstrapContext context) {
ProvisionResult provisioned = provision(context);
Path datapackRoot = provisioned.datapackRoot();
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
context.getLifecycleManager().registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
try {
discoverPack(event.registrar(), datapackRoot);
} catch (IOException e) {
throw new IllegalStateException("Unable to discover the Iris startup datapack at " + datapackRoot, e);
}
});
}
static DiscoveredDatapack discoverPack(DatapackRegistrar registrar, Path datapackRoot) throws IOException {
DiscoveredDatapack datapack = registrar.discoverPack(
datapackRoot,
PACK_ID,
configurer -> configurer
.autoEnableOnServerStart(true)
.position(true, Datapack.Position.TOP)
);
if (datapack == null) {
throw new IllegalStateException("Paper did not accept the Iris startup datapack at " + datapackRoot);
}
return datapack;
}
private static ProvisionResult provision(BootstrapContext context) {
try {
return DefaultPackBootstrapProvisioner.provision(
context.getDataDirectory(),
message -> context.getLogger().info(message)
);
} catch (IOException e) {
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
}
}
}
@@ -0,0 +1,72 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.util.List;
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)")
public class CommandDatapack implements DirectorExecutor {
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"})
public void ingest(
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
boolean restart
) {
VolmitSender sender = sender();
sender.sendMessage(C.GRAY + "Starting datapack ingest...");
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
}
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"})
public void list() {
VolmitSender sender = sender();
KList<String> configured = DatapackIngestService.collectConfiguredImports();
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
sender.sendMessage(C.GREEN + "Configured datapack imports: " + C.WHITE + configured.size());
for (String url : configured) {
sender.sendMessage(C.GRAY + " - " + C.WHITE + url);
}
sender.sendMessage(C.GREEN + "Installed datapacks: " + C.WHITE + installed.size());
for (DatapackIngestService.Entry entry : installed) {
sender.sendMessage(C.GRAY + " - " + C.WHITE + entry.id + C.GRAY + " " + (entry.versionNumber == null ? "?" : entry.versionNumber));
}
if (configured.isEmpty()) {
sender.sendMessage(C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest.");
}
}
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"})
public void remove(
@Param(description = "The datapack id (folder name) shown by /iris datapack list")
String id
) {
DatapackIngestService.remove(sender(), id);
}
}
@@ -0,0 +1,422 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import com.google.gson.JsonObject;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.runtime.ChunkClearer;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisPackBenchmarking;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
public void EngineStatus() {
Iris.service(IrisEngineSVC.class)
.engineStatus(sender());
}
@Director(description = "Send a test exception to sentry")
public void Sentry() {
Engine engine = engine();
Exception testException = new Exception("This is a test");
if (engine == null) {
Iris.reportError(testException);
return;
}
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
Iris.reportError(testException);
}
}
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", origin = DirectorOrigin.BOTH)
public void genhash(
@Param(description = "The world to hash", contextual = true)
World world,
@Param(description = "Radius in chunks around the center", defaultValue = "4")
int radius,
@Param(description = "Center chunk X", defaultValue = "0")
int centerX,
@Param(description = "Center chunk Z", defaultValue = "0")
int centerZ) {
if (world == null) {
sender().sendMessage(C.RED + "World is null.");
return;
}
VolmitSender sender = sender();
sender.sendMessage(C.GREEN + "genhash started: " + ((radius * 2 + 1) * (radius * 2 + 1)) + " chunks...");
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
}
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
long startMs = M.ms();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
long globalHash = 0L;
long solidBlocks = 0L;
Map<String, Long> histogram = new TreeMap<>();
JsonObject chunkHashes = new JsonObject();
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
org.bukkit.ChunkSnapshot snapshot;
try {
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
snapshot = loaded.getChunkSnapshot(false, false, false);
} catch (Throwable e) {
Iris.reportError(e);
sender.sendMessage(C.RED + "genhash failed at chunk " + rx + "," + rz + ": " + e.getMessage());
return;
}
long chunkHash = 0L;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int worldX = (rx << 4) + x;
int worldZ = (rz << 4) + z;
for (int y = minY; y < maxY; y++) {
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
^ ((long) worldZ * 0x165667B19E3779F9L);
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
chunkHash ^= blockHash;
if (material != org.bukkit.Material.AIR
&& material != org.bukkit.Material.CAVE_AIR
&& material != org.bukkit.Material.VOID_AIR) {
histogram.merge(material.name(), 1L, Long::sum);
solidBlocks++;
}
}
}
}
globalHash ^= chunkHash;
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
}
}
int side = radius * 2 + 1;
JsonObject result = new JsonObject();
result.addProperty("global", Long.toHexString(globalHash));
result.addProperty("chunks", side * side);
result.addProperty("solidBlocks", solidBlocks);
result.addProperty("minY", minY);
result.addProperty("maxY", maxY);
JsonObject hist = new JsonObject();
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
hist.addProperty(entry.getKey(), entry.getValue());
}
result.add("histogram", hist);
result.add("chunkHashes", chunkHashes);
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
try {
Files.writeString(out.toPath(), result.toString());
} catch (IOException e) {
Iris.reportError(e);
}
sender.sendMessage(C.GREEN + "genhash global=" + C.GOLD + Long.toHexString(globalHash)
+ C.GREEN + " chunks=" + (side * side) + " solid=" + solidBlocks
+ " in " + Form.duration((long) (M.ms() - startMs), 1));
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
}
private static long genHashMix(long z) {
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", contextual = true)
World world,
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
boolean confirm,
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
boolean freshDownload
) {
if (!confirm) {
sender().sendMessage(new String[]{
C.RED + "You should always make a backup before using this",
C.YELLOW + "Issues caused by this can be, but are not limited to:",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
C.YELLOW + " - Structures not spawning again when regenerating",
C.YELLOW + " - Caves not lining up",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
C.RED + "/iris developer update-world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
});
return;
}
File folder = world.getWorldFolder();
folder.mkdirs();
if (freshDownload) {
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
}
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack, folder);
}
@Director(description = "Test")
public void mantle(
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", defaultValue = "false")
boolean plate,
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", defaultValue = "21474836474")
String name
) throws Throwable {
Engine activeEngine = engine();
if (activeEngine == null) {
sender().sendMessage(C.RED + "Target an Iris world before reading a mantle dump.");
return;
}
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
if (plate) {
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
} catch (Throwable e) {
e.printStackTrace();
}
} else {
Matter.read(section);
}
}
if (!TectonicPlate.hasError()) {
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
}
}
@Director(description = "Test")
public void packBenchmark(
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
IrisDimension dimension,
@Param(description = "Radius in regions", defaultValue = "2048")
int radius,
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
boolean gui
) {
new IrisPackBenchmarking(dimension, radius, gui);
}
@Director(description = "Upgrade to another Minecraft version")
public void upgrade(
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
ServerConfigurator.installDataPacks(version.get(), false);
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
}
@Director(description = "test")
public void mca (
@Param(description = "The world folder to scan for .mca region files") String world) {
try {
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
for (File mca : McaFiles) {
MCAFile MCARegion = MCAUtil.read(mca);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", defaultValue = "0")
int radius
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world.");
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null.");
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "Delete started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ ". Clearing blocks to air.");
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
}
@Director(description = "Test", aliases = {"ip"})
public void network() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
Iris.info("Display Name: %s", ni.getDisplayName());
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
for (InetAddress ia : Collections.list(inetAddresses)) {
Iris.info("IP: %s", ia.getHostAddress());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// --- Regen ---
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", origin = DirectorOrigin.PLAYER, sync = true)
public void regen(
@Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5")
int radius
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "You must be in an Iris world to use regen.");
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
if (engine == null) {
sender().sendMessage(C.RED + "The engine access for this world is null. Generate nearby chunks first.");
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "Regen started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ ". Deleting and regenerating in place.");
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH)
public void goldenhash(
@Param(description = "The world to scan", contextual = true)
World world,
@Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8")
int radius,
@Param(name = "center-x", description = "Center chunk X", defaultValue = "0")
int centerX,
@Param(name = "center-z", description = "Center chunk Z", defaultValue = "0")
int centerZ,
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true")
boolean resetMantle,
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8")
int threads,
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false")
boolean deep
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "Target must be an Iris world.");
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null.");
return;
}
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ " in buffers (world untouched).");
Iris.info("goldenhash start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
}
}
@@ -0,0 +1,119 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
public class CommandEdit implements DirectorExecutor {
private boolean noStudio() {
if (!sender().isPlayer()) {
sender().sendMessage(C.RED + "Players only!");
return true;
}
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world is open!");
return true;
}
if (!engine().isStudio()) {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
if (GraphicsEnvironment.isHeadless()) {
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
return true;
}
if (!Desktop.isDesktopSupported()) {
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
return true;
}
return false;
}
@Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
if (noStudio()) {
return;
}
try {
if (biome == null || biome.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(biome.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
if (noStudio()) {
return;
}
try {
if (region == null || region.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(region.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
if (noStudio()) {
return;
}
try {
if (dimension == null || dimension.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(dimension.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
}
@@ -0,0 +1,202 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.framework.StructureReachability;
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.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome")
public void biome(
@Param(description = "The biome to look for")
IrisBiome biome,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
}
@Director(description = "Find a region")
public void region(
@Param(description = "The region to look for")
IrisRegion region,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
}
@Director(description = "Find a point of interest.")
public void poi(
@Param(description = "The type of PoI to look for.")
String type,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)")
public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
String structure
) {
VolmitSender commandSender = sender();
if (commandSender == null) {
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
return;
}
Engine e = engine();
if (e == null) {
commandSender.sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
if (IrisStructureLocator.isPlaced(e, structure)) {
EngineBukkitOps.gotoStructure(e, structure, player(), true);
return;
}
Player target = player();
if (target == null) {
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
return;
}
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.s(() -> {
try {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
Structure match = null;
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structure)) {
match = candidate;
break;
}
}
if (match == null) {
commandSender.sendMessage(C.RED + "Unknown structure: " + structure);
return;
}
if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
if (result == null || result.getLocation() == null) {
commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
return;
}
Location at = result.getLocation();
int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
BukkitPlatform.teleportAsync(target, dest);
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ());
} catch (Throwable t) {
commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structure + "'.", t);
}
});
}
@Director(description = "Find an object")
public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
String object,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
Player studioPlayer = player();
if (studioPlayer != null) {
try {
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
return;
}
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (e.hasObjectPlacement(object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
}
}
@@ -0,0 +1,642 @@
/*
* 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.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import static art.arcane.iris.core.service.EditSVC.deletingWorld;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
public class CommandIris implements DirectorExecutor {
private CommandStudio studio;
private CommandPregen pregen;
private CommandObject object;
private CommandStructure structure;
private CommandWhat what;
private CommandEdit edit;
private CommandDeveloper developer;
private CommandPack pack;
private CommandFind find;
private CommandDatapack datapack;
public static boolean worldCreation = false;
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
String WorldEngine;
String worldNameToCheck = "YourWorldName";
VolmitSender sender = Iris.getSender();
@Director(description = "Create a new world", aliases = {"c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create")
String name,
@Param(
aliases = {"dimension", "pack"},
description = "The dimension/pack to create the world with",
defaultValue = "default",
customHandler = PackDimensionTypeHandler.class
)
String type,
@Param(description = "The seed to generate the world with", defaultValue = "1337")
long seed,
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
boolean main
) {
if (name.equalsIgnoreCase("iris")) {
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (name.equalsIgnoreCase("benchmark")) {
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (IrisWorldStorage.dimensionRoot(name).exists()) {
sender().sendMessage(C.RED + "That folder already exists!");
return;
}
String resolvedType = type.equalsIgnoreCase("default")
? IrisSettings.get().getGenerator().getDefaultWorldType()
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
sender().sendMessage(C.YELLOW + "Or download manually: /iris download " + resolvedType);
return;
}
if (J.isFolia()) {
if (stageFoliaWorldCreation(name, dimension, seed, main)) {
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
}
return;
}
try {
worldCreation = true;
IrisToolbelt.createWorld()
.dimension(resolvedType)
.name(name)
.seed(seed)
.sender(sender())
.studio(false)
.create();
if (main) {
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
return new Thread(() -> updateMainWorld(name));
}));
}
} catch (Throwable e) {
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
worldCreation = false;
return;
}
worldCreation = false;
sender().sendMessage(C.GREEN + "Successfully created your world!");
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
}
private boolean updateMainWorld(String newName) {
try {
File oldLevelRoot = IrisWorldStorage.levelRoot();
File worldContainer = oldLevelRoot.getParentFile();
Properties data = ServerProperties.DATA;
try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
data.load(in);
}
File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromLegacyName(newName));
if (!sourceDimensionRoot.isDirectory()) {
throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath());
}
File newLevelRoot = new File(worldContainer, newName);
if (!newLevelRoot.exists() && !newLevelRoot.mkdirs()) {
throw new IllegalStateException("Could not create target level folder: " + newLevelRoot.getAbsolutePath());
}
for (String sub : List.of("data", "datapacks", "players")) {
File source = new File(oldLevelRoot, sub);
if (!source.exists()) {
continue;
}
IO.copyDirectory(source.toPath(), new File(newLevelRoot, sub).toPath());
}
File targetDimensionRoot = IrisWorldStorage.dimensionRoot(newLevelRoot, NamespacedKey.minecraft("overworld"));
IO.copyDirectory(sourceDimensionRoot.toPath(), targetDimensionRoot.toPath());
World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(newName)).orElse(null);
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName);
if (sourceWorld == null && stagedSeed == null) {
throw new IllegalStateException("Cannot determine the promoted world's seed.");
}
long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed();
data.setProperty("level-name", newName);
data.setProperty("level-seed", Long.toString(promotedSeed));
try (FileOutputStream out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
data.store(out, null);
}
return true;
} catch (Throwable e) {
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
Iris.reportError(e);
return false;
}
}
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
File worldFolder = IrisWorldStorage.dimensionRoot(name);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
if (installed == null) {
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
return false;
}
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
return false;
}
if (main) {
if (updateMainWorld(name)) {
sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\".");
} else {
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
return false;
}
}
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
if (main) {
sender().sendMessage(C.GREEN + "This world is now configured as main for next restart.");
}
return true;
}
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
if (worlds == null) {
worlds = yml.createSection("worlds");
}
ConfigurationSection worldSection = worlds.getConfigurationSection(worldName);
if (worldSection == null) {
worldSection = worlds.createSection(worldName);
}
String generator = "Iris:" + dimension;
worldSection.set("generator", generator);
if (seed != null) {
worldSection.set("seed", seed);
}
try {
yml.save(BUKKIT_YML);
Iris.info("Registered \"" + worldName + "\" in bukkit.yml");
return true;
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage());
Iris.error("Failed to update bukkit.yml!");
Iris.reportError(e);
return false;
}
}
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
public void teleport(
@Param(description = "World to teleport to")
World world,
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
Player player
) {
if (player == null && sender().isPlayer())
player = sender().player();
final Player target = player;
if (target == null) {
sender().sendMessage(C.RED + "The specified player does not exist.");
return;
}
final Location spawn = world.getSpawnLocation();
final Runnable teleportTask = () -> {
BukkitPlatform.teleportAsync(target, spawn);
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
};
if (!J.runEntity(target, teleportTask)) {
teleportTask.run();
}
}
@Director(description = "Print version information")
public void version() {
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
}
/*
/todo
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Director(description = "Print world height information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
sender().sendMessage(C.GREEN + "Total Height: " + (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.", 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(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
}
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
sender().sendMessage(C.GRAY + "- " +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", aliases = {"rm"}, sync = true)
public void remove(
@Param(description = "The world to remove")
World world,
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
boolean delete
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
if (!IrisToolbelt.evacuate(world)) {
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
return;
}
if (!WorldLifecycleService.get().unload(world, false)) {
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
return;
}
try {
if (IrisToolbelt.removeWorld(world)) {
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
} else {
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
}
IrisToolbelt.evacuate(world, "Deleting world");
deletingWorld = true;
if (!delete) {
deletingWorld = false;
return;
}
VolmitSender sender = sender();
J.a(() -> {
int retries = 12;
if (deleteDirectory(world.getWorldFolder())) {
sender.sendMessage(C.GREEN + "Successfully removed world folder");
} else {
while(true){
if (deleteDirectory(world.getWorldFolder())){
sender.sendMessage(C.GREEN + "Successfully removed world folder");
break;
}
retries--;
if (retries == 0){
sender.sendMessage(C.RED + "Failed to remove world folder");
break;
}
J.sleep(3000);
}
}
deletingWorld = false;
});
}
public static boolean deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDirectory(children[i]);
if (!success) {
return false;
}
}
}
return dir.delete();
}
@Director(description = "Toggle debug")
public void debug() {
boolean to = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(C.GREEN + "Set debug to: " + to);
}
@Director(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
if (PackDownloader.isDefaultOverworld(pack)) {
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + " (beta release)" + (overwrite ? " overwriting" : ""));
Iris.service(StudioSVC.class).downloadDefaultOverworld(sender(), overwrite);
} else {
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
}
ServerConfigurator.installDataPacksIfChanged(true);
}
@Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER)
public void metrics() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world");
return;
}
sender().sendMessage(C.GREEN + "Sending metrics...");
engine().printMetrics(sender());
}
@Director(description = "Reload configuration file (this is also done automatically)")
public void reload() {
IrisSettings.invalidate();
IrisSettings.get();
sender().sendMessage(C.GREEN + "Hotloaded settings");
}
@Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true)
public void unloadWorld(
@Param(description = "The world to unload")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
try {
IrisToolbelt.evacuate(world);
boolean unloaded = WorldLifecycleService.get().unload(world, false);
if (unloaded) {
sender().sendMessage(C.GREEN + "World unloaded successfully.");
} else {
sender().sendMessage(C.RED + "Failed to unload the world.");
}
} catch (Exception e) {
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
}
}
@Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
public void loadWorld(
@Param(description = "The name of the world to load")
String world
) {
worldNameToCheck = world;
boolean worldExists = doesWorldExist(worldNameToCheck);
WorldEngine = world;
if (!worldExists) {
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
return;
}
File directory = new File(IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(world)), "dimensions");
String dimension = null;
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String fileName = file.getName();
if (fileName.endsWith(".json")) {
dimension = fileName.substring(0, fileName.length() - 5);
sender().sendMessage(C.BLUE + "Generator: " + dimension);
}
}
}
}
} else {
sender().sendMessage(C.GOLD + world + " is not an iris world.");
return;
}
if (dimension == null) {
sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + ".");
return;
}
sender().sendMessage(C.GREEN + "Loading world: " + world);
if (!registerWorldInBukkitYml(world, dimension, null)) {
return;
}
if (J.isFolia()) {
sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\".");
return;
}
Iris.instance.checkForBukkitWorlds(world::equals);
sender().sendMessage(C.GREEN + world + " loaded successfully.");
}
@Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true)
public void evacuate(
@Param(description = "Evacuate the world")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
IrisToolbelt.evacuate(world);
}
boolean doesWorldExist(String worldName) {
File worldDirectory = IrisWorldStorage.dimensionRoot(worldName);
return worldDirectory.exists() && worldDirectory.isDirectory();
}
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> options = new LinkedHashSet<>();
options.add("default");
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles();
if (packs != null) {
for (File pack : packs) {
if (pack == null || !pack.isDirectory()) {
continue;
}
options.add(pack.getName());
try {
IrisData data = IrisData.get(pack);
for (String key : data.getDimensionLoader().getPossibleKeys()) {
options.add(key);
options.add(pack.getName() + ":" + key);
}
} catch (Throwable ex) {
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
pack.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
}
return new KList<>(options);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.trim().isEmpty()) {
throw new DirectorParsingException("World type cannot be empty");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -16,42 +16,171 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
package art.arcane.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.link.WorldEditLink;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.service.ObjectSVC;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.service.WandSVC;
import com.volmit.iris.core.tools.IrisConverter;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.util.data.Cuboid;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.data.registry.Materials;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.math.Direction;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.Queue;
import org.bukkit.*;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.StudioMode;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.data.registry.Materials;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
public class CommandObject implements DecreeExecutor {
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation")
public class CommandObject implements DirectorExecutor {
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", sync = true)
public void studio(
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
long seed
) {
VolmitSender commandSender = sender();
Map<String, IrisData> sources = new LinkedHashMap<>();
IrisDimension hostDimension = dimension;
if (dimension != null) {
IrisData data = dimension.getLoader();
if (data == null) {
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
}
sources.put(data.getDataFolder().getName(), data);
} else {
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
File[] packs = workspace == null ? null : workspace.listFiles();
if (packs != null) {
Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File pack : packs) {
if (!pack.isDirectory()) continue;
File dimensionsDir = new File(pack, "dimensions");
if (!dimensionsDir.isDirectory()) continue;
IrisData data = IrisData.get(pack);
String[] keys = data.getObjectLoader().getPossibleKeys();
if (keys == null || keys.length == 0) continue;
sources.put(pack.getName(), data);
if (hostDimension == null) {
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
if (dimFiles != null && dimFiles.length > 0) {
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
if (loaded != null) {
hostDimension = loaded;
}
}
}
}
}
}
if (hostDimension == null || sources.isEmpty()) {
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
return;
}
int totalObjects = 0;
for (IrisData d : sources.values()) {
String[] k = d.getObjectLoader().getPossibleKeys();
if (k != null) totalObjects += k.length;
}
if (totalObjects == 0) {
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
return;
}
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
ObjectStudioActivation.activate(hostDimension.getLoadKey());
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
String scope = dimension == null
? ("all packs [" + sources.size() + "]")
: ("\"" + hostDimension.getName() + "\"");
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
+ totalObjects + " objects)");
IrisDimension finalHost = hostDimension;
try {
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
} catch (Throwable e) {
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
}
}
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
Material.POPPY, Material.DANDELION);
@@ -70,7 +199,8 @@ public class CommandObject implements DecreeExecutor {
}
@Override
public void set(int x, int y, int z, BlockData d) {
public void set(int x, int y, int z, PlatformBlockState s) {
BlockData d = (BlockData) s.nativeHandle();
Block block = world.getBlockAt(x, y, z);
//Prevent blocks being set in or bellow bedrock
@@ -79,14 +209,14 @@ public class CommandObject implements DecreeExecutor {
futureBlockChanges.put(block, block.getBlockData());
if (d instanceof IrisCustomData data) {
block.setBlockData(data.getBase());
block.setBlockData(data.getBase(), false);
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
} else block.setBlockData(d);
} else block.setBlockData(d, false);
}
@Override
public BlockData get(int x, int y, int z) {
return world.getBlockAt(x, y, z).getBlockData();
public PlatformBlockState get(int x, int y, int z) {
return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData());
}
@Override
@@ -124,6 +254,16 @@ public class CommandObject implements DecreeExecutor {
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 null;
@@ -131,21 +271,21 @@ public class CommandObject implements DecreeExecutor {
};
}
@Decree(description = "Check the composition of an object")
@Director(description = "Check the composition of an object")
public void analyze(
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
String object
) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
Queue<BlockData> queue = o.getBlocks().enqueueValues();
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 = queue.next();
BlockData block = (BlockData) queue.next().nativeHandle();
//unsorted.put(block.getMaterial(), block);
@@ -199,9 +339,9 @@ public class CommandObject implements DecreeExecutor {
}
}
@Decree(description = "Shrink an object to its minimum size")
@Director(description = "Shrink an object to its minimum size")
public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
o.shrinkwrap();
sender().sendMessage("New Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
@@ -213,7 +353,200 @@ public class CommandObject implements DecreeExecutor {
}
}
@Decree(description = "Convert .schem files in the 'convert' folder to .iob files.")
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
origin = DirectorOrigin.BOTH)
public void plausibilize(
@Param(description = "Object key, prefix (trees/), or filesystem path",
customHandler = ObjectTargetHandler.class)
String target,
@Param(description = "DEFAULT: tentacle logs, delete orphans. NORMALIZE: + flip persistent=false. FOLIAGE_OVERATURE: add leaves to bridge orphans, no deletions. SMOKE: wipe & repaint canopy shell.",
defaultValue = "DEFAULT")
PlausibilizeMode mode,
@Param(description = "Analyze only, do not write", defaultValue = "false")
boolean dryRun,
@Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2")
int radius
) {
List<Target> targets = resolveTargets(target);
if (targets.isEmpty()) {
sender().sendMessage(C.RED + "No objects matched: " + target);
return;
}
sender().sendMessage(C.IRIS + "Plausibilize [" + mode.name()
+ (dryRun ? " DRY" : "")
+ (mode == PlausibilizeMode.SMOKE ? " r=" + radius : "")
+ "] queued " + targets.size() + " object(s)");
org.bukkit.command.CommandSender s = sender();
J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s));
}
private List<Target> resolveTargets(String target) {
List<Target> out = new ArrayList<>();
if (target == null || target.isEmpty()) {
return out;
}
File direct = new File(target);
if (direct.isFile() && target.toLowerCase().endsWith(".iob")) {
out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct));
return out;
}
if (direct.isDirectory()) {
walkIob(direct, direct, out);
return out;
}
IrisData irisData = data();
if (irisData != null) {
ResourceLoader<IrisObject> loader = irisData.getObjectLoader();
if (!target.endsWith("/") && loader.findFile(target) != null) {
out.add(new Target(target, null));
return out;
}
String prefix = target.endsWith("/") ? target : target + "/";
for (String k : loader.getPossibleKeys()) {
if (k.startsWith(prefix)) {
out.add(new Target(k, null));
}
}
return out;
}
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles(File::isDirectory);
if (packs != null) {
for (File pack : packs) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) continue;
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
walkIob(candidateDir, objectsRoot, out);
}
}
}
return out;
}
private static void walkIob(File root, File keyRoot, List<Target> out) {
File[] kids = root.listFiles();
if (kids == null) return;
for (File f : kids) {
if (f.isDirectory()) {
walkIob(f, keyRoot, out);
} else if (f.getName().toLowerCase().endsWith(".iob")) {
String rel = keyRoot.toPath().relativize(f.toPath()).toString()
.replace(File.separatorChar, '/')
.replaceAll("\\.iob$", "");
out.add(new Target(rel, f));
}
}
}
private record Target(String key, File file) {
}
private static void runPlausibilize(
List<Target> targets,
boolean dryRun,
PlausibilizeMode mode,
int radius,
org.bukkit.command.CommandSender s
) {
int processed = 0;
int skipped = 0;
int failed = 0;
int changed = 0;
long totalLogsAdded = 0L;
long totalLeavesAdded = 0L;
long totalLeavesRemoved = 0L;
long totalNormalized = 0L;
long totalUnreachableAfter = 0L;
int progressStep = Math.max(1, targets.size() / 20);
int index = 0;
for (Target t : targets) {
index++;
try {
IrisObject o = loadTarget(t);
if (o == null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": failed to load");
skipped++;
continue;
}
TreePlausibilizer.Result r = dryRun
? TreePlausibilizer.analyze(o, mode, radius)
: TreePlausibilizer.apply(o, mode, radius);
if (r.skipReason() != null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason());
skipped++;
continue;
}
boolean touched = r.logsAdded() > 0 || r.leavesAdded() > 0
|| r.leavesRemoved() > 0 || r.leavesNormalized() > 0;
if (!dryRun && touched) {
File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file();
if (dest != null) {
o.write(dest);
changed++;
}
}
processed++;
totalLogsAdded += r.logsAdded();
totalLeavesAdded += r.leavesAdded();
totalLeavesRemoved += r.leavesRemoved();
totalNormalized += r.leavesNormalized();
totalUnreachableAfter += r.unreachableAfter();
if (touched || targets.size() == 1) {
s.sendMessage(C.GRAY + " " + t.key()
+ C.WHITE + " +" + r.logsAdded() + " logs"
+ C.WHITE + " +" + r.leavesAdded() + " leaves"
+ C.WHITE + " -" + r.leavesRemoved() + " removed"
+ (r.leavesNormalized() > 0 ? C.WHITE + " ~" + r.leavesNormalized() + " normalized" : "")
+ (r.unreachableAfter() > 0 ? C.YELLOW + " " + r.unreachableAfter() + " unreachable" : ""));
}
if (targets.size() > 1 && index % progressStep == 0) {
s.sendMessage(C.IRIS + " [" + index + "/" + targets.size() + "]");
}
} catch (Throwable e) {
s.sendMessage(C.RED + " fail " + t.key() + ": " + e.getClass().getSimpleName()
+ ": " + e.getMessage());
e.printStackTrace();
failed++;
}
}
s.sendMessage(C.IRIS + "Done: " + processed + " processed, " + changed + " changed, "
+ skipped + " skipped, " + failed + " failed");
s.sendMessage(C.IRIS + "Totals: +" + totalLogsAdded + " logs, +" + totalLeavesAdded + " leaves, -"
+ totalLeavesRemoved + " removed, ~" + totalNormalized + " normalized, "
+ totalUnreachableAfter + " unreachable");
}
private static IrisObject loadTarget(Target t) throws IOException {
if (t.file() != null) {
IrisObject o = new IrisObject();
o.read(t.file());
o.setLoadFile(t.file());
return o;
}
return IrisData.loadAnyObject(t.key(), null);
}
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
public void convert () {
try {
IrisConverter.convertSchematics(sender());
@@ -223,13 +556,13 @@ public class CommandObject implements DecreeExecutor {
}
@Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d")
@Director(description = "Get a powder that reveals objects", aliases = "d")
public void dust() {
player().getInventory().addItem(WandSVC.createDust());
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
}
@Decree(description = "Contract a selection based on your looking direction", aliases = "-")
@Director(description = "Contract a selection based on your looking direction", aliases = "-")
public void contract(
@Param(description = "The amount to inset by", defaultValue = "1")
int amount
@@ -250,7 +583,7 @@ public class CommandObject implements DecreeExecutor {
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d, -amount);
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
@@ -258,7 +591,7 @@ public class CommandObject implements DecreeExecutor {
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
}
@Decree(description = "Set point 1 to look", aliases = "p1")
@Director(description = "Set point 1 to look", aliases = "p1")
public void position1(
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
boolean here
@@ -280,11 +613,11 @@ public class CommandObject implements DecreeExecutor {
} else {
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
}
@Decree(description = "Set point 2 to look", aliases = "p2")
@Director(description = "Set point 2 to look", aliases = "p2")
public void position2(
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
boolean here
@@ -307,11 +640,11 @@ public class CommandObject implements DecreeExecutor {
} else {
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
}
@Decree(description = "Paste an object", sync = true)
@Director(description = "Paste an object", sync = true)
public void paste(
@Param(description = "The object to paste", customHandler = ObjectHandler.class)
String object,
@@ -325,7 +658,7 @@ public class CommandObject implements DecreeExecutor {
// @Param(description = "The scale interpolator to use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator
) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
if (scale > maxScale) {
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
@@ -351,8 +684,9 @@ public class CommandObject implements DecreeExecutor {
Iris.service(ObjectSVC.class).addChanges(futureChanges);
if (edit) {
ItemStack newWand = WandSVC.createWand(block.clone().subtract(o.getCenter()).add(o.getW() - 1,
o.getH() + o.getCenter().clone().getY() - 1, o.getD() - 1), block.clone().subtract(o.getCenter().clone().setY(0)));
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
if (WandSVC.isWand(wand)) {
wand = newWand;
player().getInventory().setItemInMainHand(wand);
@@ -372,7 +706,7 @@ public class CommandObject implements DecreeExecutor {
}
}
@Decree(description = "Save an object")
@Director(description = "Save an object")
public void save(
@Param(description = "The dimension to store the object in", contextual = true)
IrisDimension dimension,
@@ -407,7 +741,7 @@ public class CommandObject implements DecreeExecutor {
sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name);
}
@Decree(description = "Shift a selection in your looking direction", aliases = "-")
@Director(description = "Shift a selection in your looking direction")
public void shift(
@Param(description = "The amount to shift by", defaultValue = "1")
int amount
@@ -438,7 +772,7 @@ public class CommandObject implements DecreeExecutor {
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
}
@Decree(description = "Undo a number of pastes", aliases = "-")
@Director(description = "Undo a number of pastes", aliases = "u")
public void undo(
@Param(description = "The amount of pastes to undo", defaultValue = "1")
int amount
@@ -449,7 +783,7 @@ public class CommandObject implements DecreeExecutor {
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
}
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER)
public void we() {
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know.");
@@ -467,14 +801,14 @@ public class CommandObject implements DecreeExecutor {
sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!");
}
@Decree(description = "Get an object wand", sync = true)
@Director(description = "Get an object wand", sync = true)
public void wand() {
player().getInventory().addItem(WandSVC.createWand());
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
sender().sendMessage(C.GREEN + "Poof! Good luck building!");
}
@Decree(name = "x&y", description = "Autoselect up, down & out", sync = true)
@Director(name = "x&y", description = "Autoselect up, down & out", sync = true)
public void xay() {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(C.YELLOW + "Hold your wand!");
@@ -525,7 +859,7 @@ public class CommandObject implements DecreeExecutor {
sender().sendMessage(C.GREEN + "Auto-select complete!");
}
@Decree(name = "x+y", description = "Autoselect up & out", sync = true)
@Director(name = "x+y", description = "Autoselect up & out", sync = true)
public void xpy() {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(C.YELLOW + "Hold your wand!");
@@ -0,0 +1,259 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.File;
import java.util.List;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
public class CommandPack implements DirectorExecutor {
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"})
public void validate(
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
String pack
) {
VolmitSender s = sender();
File packsRoot = Iris.instance.getDataFolder("packs");
if (!packsRoot.isDirectory()) {
s.sendMessage(C.RED + "packs/ folder not found.");
return;
}
if (pack == null || pack.isBlank()) {
File[] dirs = packsRoot.listFiles(File::isDirectory);
if (dirs == null || dirs.length == 0) {
s.sendMessage(C.YELLOW + "No packs to validate.");
return;
}
int broken = 0;
for (File dir : dirs) {
PackValidationResult result = runValidate(s, dir);
if (result != null && !result.isLoadable()) {
broken++;
}
}
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
return;
}
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
if (target == null) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
runValidate(s, target);
}
@Director(description = "Preview or apply unused-resource cleanup", aliases = {"c"})
public void cleanup(
@Param(description = "The pack folder name to clean")
String pack,
@Param(description = "preview or apply", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
if (!result.success()) {
s.sendMessage(C.RED + result.error());
reportPaths(s, result.quarantinedPaths(), "still quarantined");
return;
}
if (!result.changed()) {
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Quarantined " + result.quarantinedPaths().size()
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
reportPaths(s, result.quarantinedPaths(), "quarantined");
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(C.RED + "Cleanup mode must be preview or apply.");
return;
}
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
if (!preview.success()) {
s.sendMessage(C.RED + preview.error());
return;
}
if (!preview.hasCandidates()) {
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
return;
}
s.sendMessage(C.YELLOW + "Cleanup preview for pack '" + pack + "': "
+ preview.candidatePaths().size() + " candidate(s). No files were changed.");
reportPaths(s, preview.candidatePaths(), "candidate");
s.sendMessage(C.GRAY + "Run /iris pack cleanup " + pack + " mode=apply to quarantine these candidates after a fresh scan.");
}
@Director(description = "Preview or apply restoration of the latest quarantine", aliases = {"r"})
public void restore(
@Param(description = "The pack folder name to restore")
String pack,
@Param(description = "preview or apply", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
if (!result.conflicts().isEmpty()) {
s.sendMessage(C.RED + "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
reportPaths(s, result.conflicts(), "conflict");
return;
}
if (!result.success()) {
s.sendMessage(C.RED + result.error());
return;
}
if (!result.changed()) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Restored " + result.restoredPaths().size()
+ " file(s) from " + result.dumpPath() + ".");
reportPaths(s, result.restoredPaths(), "restored");
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(C.RED + "Restore mode must be preview or apply.");
return;
}
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
if (!preview.success()) {
s.sendMessage(C.RED + preview.error());
return;
}
if (!preview.hasFiles()) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.YELLOW + "Restore preview for " + preview.dumpPath() + ": "
+ preview.filePaths().size() + " file(s). No files were changed.");
reportPaths(s, preview.filePaths(), "file");
if (!preview.conflicts().isEmpty()) {
s.sendMessage(C.RED + "Restore is blocked by " + preview.conflicts().size() + " existing destination(s).");
reportPaths(s, preview.conflicts(), "conflict");
return;
}
s.sendMessage(C.GRAY + "Run /iris pack restore " + pack + " mode=apply to restore after a fresh conflict check.");
}
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
public void status(
@Param(description = "The pack folder name", defaultValue = "")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
return;
}
PackValidationRegistry.snapshot().forEach((name, result) -> {
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
s.sendMessage(tag + C.RESET + " " + name
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
+ ", warnings=" + result.getWarnings().size() + ")");
});
return;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
return;
}
reportResult(s, result);
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
return null;
}
}
private void reportResult(VolmitSender s, PackValidationResult result) {
if (result.isLoadable()) {
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
+ C.GRAY + " (warnings=" + result.getWarnings().size() + ")");
} else {
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
for (String reason : result.getBlockingErrors()) {
s.sendMessage(C.RED + " - " + reason);
}
}
int wMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < wMax; i++) {
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
}
if (result.getWarnings().size() > wMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
}
}
private File findPack(VolmitSender sender, String pack) {
if (pack == null || pack.isBlank()) {
sender.sendMessage(C.RED + "You must specify a pack name.");
return null;
}
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
if (packFolder == null) {
sender.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return null;
}
return packFolder;
}
private void reportPaths(VolmitSender sender, List<String> paths, String label) {
int max = Math.min(10, paths.size());
for (int i = 0; i < max; i++) {
sender.sendMessage(C.GRAY + " - " + label + ": " + paths.get(i));
}
if (paths.size() > max) {
sender.sendMessage(C.GRAY + " ... and " + (paths.size() - max) + " more.");
}
}
}
@@ -0,0 +1,123 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.World;
import org.bukkit.util.Vector;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
public class CommandPregen implements DirectorExecutor {
@Director(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center,
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
boolean gui,
@Param(name = "serial", description = "Generate only one chunk at a time", defaultValue = "false")
boolean serial
) {
if (radius <= 0) {
sender().sendMessage(C.RED + "Pregen radius must be greater than zero blocks.");
return;
}
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
sender().sendMessage(C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server.");
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PregenTask task = PregenTask
.builder()
.center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(gui)
.radiusX(radius)
.radiusZ(radius)
.build();
if (serial) {
IrisToolbelt.pregenerateSerial(task, world);
} else {
IrisToolbelt.pregenerate(task, world);
}
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Failed to start pregeneration. See console for details.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Director(description = "Stop the active pregeneration task", aliases = "x")
public void stop() {
if (PregeneratorJob.shutdownInstance()) {
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
sender().sendMessage(message);
Iris.info(message);
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"})
public void pause() {
if (PregeneratorJob.pauseResume()) {
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
}
}
@Director(description = "Show the active pregeneration status")
public void status() {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
sender().sendMessage(C.YELLOW + "No active pregeneration task.");
return;
}
String world = progress.worldName() == null ? "?" : progress.worldName();
sender().sendMessage(C.GREEN + "Pregen " + C.GOLD + world + C.GREEN + ": " + C.GOLD + Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks())
+ C.GREEN + " (" + C.GOLD + String.format("%.1f", progress.percent()) + "%" + C.GREEN + ")"
+ (progress.paused() ? C.YELLOW + " PAUSED" : ""));
sender().sendMessage(C.GREEN + "Speed: " + C.GOLD + Form.f((int) progress.chunksPerSecond()) + "/s" + C.GREEN
+ " ETA: " + C.GOLD + Form.duration(progress.eta(), 2) + C.GREEN
+ " Elapsed: " + C.GOLD + Form.duration(progress.elapsed(), 2) + C.GREEN
+ " Method: " + C.GOLD + progress.method()
+ (progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""));
}
}
@@ -0,0 +1,284 @@
/*
* 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.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.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStructure;
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.volmlib.util.math.RNG;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)")
public class CommandStructure implements DirectorExecutor {
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
public void list(
@Param(description = "The dimension whose pack to index", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
File file = StructureIndexService.write(data);
sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath());
}
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
public void importAll(
@Param(description = "The dimension whose pack to import into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
StructureCaptureImporter.Report captured = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed.");
sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect.");
}
@Director(name = "capture", description = "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import.", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
public void capture(
@Param(description = "The dimension whose pack to capture into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
}
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
public void verify(
@Param(description = "The dimension to verify", aliases = "dim")
IrisDimension dimension,
@Param(description = "Search radius in chunks around the origin (larger is much slower)", defaultValue = "48")
int radius
) {
World world = resolveIrisWorld(dimension);
if (world == null) {
sender().sendMessage(C.RED + "No loaded Iris world found for " + dimension.getLoadKey() + ". Join or create one first (the search runs against a live world).");
return;
}
boolean senderIsPlayer = sender() != null && sender().isPlayer();
Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation();
int searchRadius = Math.max(1, Math.min(radius, 1000));
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
Engine engine = null;
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null) {
engine = access.getEngine();
}
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
int found = 0;
int missing = 0;
int unreachable = 0;
int irisPlaced = 0;
KList<String> notFound = new KList<>();
KList<String> cannotGenerate = new KList<>();
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure);
String keyName = key == null ? structure.toString() : key.toString();
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
if (engine != null && !isIrisPlaced && !isReachable) {
unreachable++;
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
continue;
}
if (isIrisPlaced) {
irisPlaced++;
}
try {
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
if (result != null && result.getLocation() != null) {
found++;
Location l = result.getLocation();
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
} else {
missing++;
notFound.add(keyName);
}
} catch (Throwable e) {
missing++;
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
}
}
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
if (!cannotGenerate.isEmpty()) {
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
}
if (!notFound.isEmpty()) {
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
}
}
private World resolveIrisWorld(IrisDimension dimension) {
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
return player().getWorld();
}
World fallback = null;
for (World w : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(w)) {
continue;
}
if (fallback == null) {
fallback = w;
}
PlatformChunkGenerator gen = IrisToolbelt.access(w);
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
return w;
}
}
return fallback;
}
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
public void info(
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to inspect")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0);
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
return;
}
int minX = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (PlacedStructurePiece p : pieces) {
minX = Math.min(minX, p.getMinX());
minZ = Math.min(minZ, p.getMinZ());
maxX = Math.max(maxX, p.getMaxX());
maxZ = Math.max(maxZ, p.getMaxZ());
}
sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)");
}
@Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
public void place(
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to place")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
Location loc = player().getLocation();
StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces");
return;
}
Map<Block, BlockData> future = new HashMap<>();
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
for (PlacedStructurePiece p : pieces) {
IrisObjectPlacement config = new IrisObjectPlacement();
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
config.setRotation(p.getRotation());
config.getPlace().add(p.getObject().getLoadKey());
if (!s.getEdit().isEmpty()) {
config.setEdit(s.getEdit());
}
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
}
sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location.");
}
}
@@ -16,53 +16,71 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
package art.arcane.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.gui.NoiseExplorerGUI;
import com.volmit.iris.core.gui.VisionGUI;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.project.IrisProject;
import com.volmit.iris.core.service.ConversionSVC;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.collection.KSet;
import com.volmit.iris.util.decree.DecreeContext;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.function.Function2;
import com.volmit.iris.util.function.NoiseProvider;
import com.volmit.iris.util.interpolation.InterpolationMethod;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.json.JSONArray;
import com.volmit.iris.util.json.JSONObject;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.math.Position2;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.math.Spiraler;
import com.volmit.iris.util.noise.CNG;
import com.volmit.iris.util.parallel.BurstExecutor;
import com.volmit.iris.util.parallel.MultiBurst;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.O;
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import com.volmit.iris.util.scheduling.jobs.QueueJob;
import io.papermc.lib.PaperLib;
import org.bukkit.*;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.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.Function2;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import java.io.File;
@@ -73,17 +91,13 @@ import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
public class CommandStudio implements DecreeExecutor {
private CommandFind find;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands")
public class CommandStudio implements DirectorExecutor {
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
@@ -91,23 +105,9 @@ public class CommandStudio implements DecreeExecutor {
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
}
@Decree(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
String branch,
@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
boolean trim,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
new CommandIris().download(pack, branch, trim, overwrite);
}
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
@Director(description = "Open a new studio world", aliases = "o", sync = true)
public void open(
@Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim")
@Param(description = "The dimension pack to open a studio for", aliases = "dim", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
long seed) {
@@ -115,181 +115,136 @@ public class CommandStudio implements DecreeExecutor {
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
}
@Decree(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
@Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH)
public void importvanilla(
@Param(description = "The dimension pack to import vanilla content into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(description = "How many variants to capture per tree/object feature", defaultValue = "3")
int variants,
@Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", defaultValue = "true")
boolean structures
) {
if (dimension == null) {
sender().sendMessage(C.RED + "Provide a dimension pack: /iris std importvanilla pack=<dimension>");
return;
}
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
VolmitSender sender = sender();
sender.sendMessage(C.GREEN + "Importing vanilla content into " + C.WHITE + dimension.getLoadKey() + C.GREEN + "...");
FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender);
int imported = features.imported();
int failed = features.failed();
if (structures) {
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender);
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender);
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender);
imported += jigsaws.imported() + templates.imported() + groups.imported();
failed += jigsaws.failed() + templates.failed() + groups.failed();
}
sender.sendMessage(C.GREEN + "importvanilla complete: " + C.WHITE + imported + C.GREEN + " objects/structures written, " + C.WHITE + failed + C.GREEN + " failed.");
sender.sendMessage(C.GRAY + "Trees/objects are under objects/vanilla/...; reference them from biome object placements.");
}
@Director(description = "Open VSCode for a dimension", aliases = {"vsc"})
public void vscode(
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim")
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack");
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
}
@Decree(description = "Close an open studio project", aliases = {"x", "c"}, sync = true)
@Director(description = "Close an open studio project", aliases = {"x"}, sync = true)
public void close() {
VolmitSender commandSender = sender();
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No open studio projects.");
commandSender.sendMessage(C.RED + "No open studio projects.");
return;
}
Iris.service(StudioSVC.class).close();
sender().sendMessage(C.GREEN + "Project Closed.");
commandSender.sendMessage(C.YELLOW + "Closing studio...");
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
if (throwable != null) {
commandSender.sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
return;
}
if (result != null && result.failureCause() != null) {
commandSender.sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
return;
}
if (result != null && result.startupCleanupQueued()) {
commandSender.sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
return;
}
commandSender.sendMessage(C.GREEN + "Studio closed.");
}));
}
@Decree(description = "Create a new studio project", aliases = "+", sync = true)
@Director(description = "Create a new studio project", aliases = "+", sync = true)
public void create(
@Param(description = "The name of this new Iris Project.")
@Param(description = "The name of this new Iris Project.", defaultValue = "studio")
String name,
@Param(description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", contextual = true)
@Param(
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.",
contextual = true,
customHandler = NullableDimensionHandler.class
)
IrisDimension template) {
String projectName = name;
if (name.equals("studio")) {
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
int suffix = 2;
while (new File(workspace, projectName).exists()) {
projectName = "studio" + suffix++;
}
}
if (template != null) {
Iris.service(StudioSVC.class).create(sender(), name, template.getLoadKey());
Iris.service(StudioSVC.class).create(sender(), projectName, template.getLoadKey());
} else {
Iris.service(StudioSVC.class).create(sender(), name);
Iris.service(StudioSVC.class).create(sender(), projectName);
}
}
@Decree(description = "Get the version of a pack")
@Director(description = "Get the version of a pack")
public void version(
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true)
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true, customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
}
@Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5")
int radius
) {
if (IrisToolbelt.isIrisWorld(player().getWorld())) {
VolmitSender sender = sender();
J.a(() -> {
DecreeContext.touch(sender);
PlatformChunkGenerator plat = IrisToolbelt.access(player().getWorld());
Engine engine = plat.getEngine();
try {
Chunk cx = player().getLocation().getChunk();
KList<Runnable> js = new KList<>();
BurstExecutor b = MultiBurst.burst.burst();
b.setMulticore(false);
int rad = engine.getMantle().getRadius();
for (int i = -(radius + rad); i <= radius + rad; i++) {
for (int j = -(radius + rad); j <= radius + rad; j++) {
engine.getMantle().getMantle().deleteChunk(i + cx.getX(), j + cx.getZ());
}
}
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
int finalJ = j;
int finalI = i;
b.queue(() -> plat.injectChunkReplacement(player().getWorld(), finalI + cx.getX(), finalJ + cx.getZ(), (f) -> {
synchronized (js) {
js.add(f);
}
}));
}
}
b.complete();
sender().sendMessage(C.GREEN + "Regenerating " + Form.f(js.size()) + " Sections");
QueueJob<Runnable> r = new QueueJob<>() {
final KList<Future<?>> futures = new KList<>();
@Override
public void execute(Runnable runnable) {
futures.add(J.sfut(runnable));
if (futures.size() > 64) {
while (futures.isNotEmpty()) {
try {
futures.remove(0).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
}
@Override
public String getName() {
return "Regenerating";
}
};
r.queue(js);
r.execute(sender());
} catch (Throwable e) {
sender().sendMessage("Unable to parse view-distance");
}
});
} else {
sender().sendMessage(C.RED + "You must be in an Iris World to use regen!");
}
}
@Decree(description = "Convert objects in the \"convert\" folder")
public void convert() {
Iris.service(ConversionSVC.class).check(sender());
//IrisConverter.convertSchematics(sender());
}
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
public void execute(
@Param(description = "The script to run")
IrisScript script
) {
engine().getExecution().execute(script.getLoadKey());
}
@Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
public void noise() {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
NoiseExplorerGUI.launch();
}
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
public void charge() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!");
return;
}
sender().sendMessage(C.GREEN + "Charging spawners!");
engine().getWorldManager().chargeEnergy();
}
@Decree(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"})
public void explore(
@Param(description = "The generator to explore", contextual = true)
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap"})
public void noise(
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
IrisGenerator generator,
@Param(description = "The seed to generate with", defaultValue = "12345")
@Param(description = "The seed to preview the generator with", defaultValue = "12345")
long seed
) {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
Supplier<Function2<Double, Double, Double>> l = () -> {
if (generator == null) {
return (x, z) -> 0D;
}
return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
};
NoiseExplorerGUI.launch(l, "Custom Generator");
}
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
public void hotload() {
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world open!");
if (generator == null) {
NoiseExplorerGUI.launch();
return;
}
Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload();
sender().sendMessage(C.GREEN + "Hotloaded");
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
NoiseExplorerGUI.launch(supplier, "Custom Generator");
}
@Decree(description = "Show loot if a chest were right here", origin = DecreeOrigin.PLAYER, sync = true)
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
public void loot(
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
boolean fast,
@@ -298,11 +253,11 @@ public class CommandStudio implements DecreeExecutor {
) {
if (noStudio()) return;
KList<IrisLootTable> tables = engine().getLootTables(RNG.r, player().getLocation().getBlock());
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock());
Inventory inv = Bukkit.createInventory(null, 27 * 2);
try {
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
EngineBukkitOps.addItems(engine(), true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
@@ -313,11 +268,15 @@ public class CommandStudio implements DecreeExecutor {
O<Integer> ta = new O<>();
ta.set(-1);
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
var sender = sender();
var player = player();
var engine = engine();
ta.set(J.sr(() ->
{
if (!player().getOpenInventory().getType().equals(InventoryType.CHEST)) {
Bukkit.getScheduler().cancelTask(ta.get());
sender().sendMessage(C.GREEN + "Opened inventory!");
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
J.csr(ta.get());
sender.sendMessage(C.GREEN + "Opened inventory!");
return;
}
@@ -325,81 +284,51 @@ public class CommandStudio implements DecreeExecutor {
inv.clear();
}
engine().addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
}, 0, fast ? 5 : 35));
EngineBukkitOps.addItems(engine, true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
}, fast ? 5 : 35));
sender().sendMessage(C.GREEN + "Opening inventory now!");
player().openInventory(inv);
}
@Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER)
public void distances(@Param(description = "The radius in chunks") int radius) {
@Director(description = "Calculate the chance for each region to generate", origin = DirectorOrigin.PLAYER)
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
var engine = engine();
if (engine == null) {
sender().sendMessage(C.RED + "Only works in an Iris world!");
return;
}
var sender = sender();
int d = radius * 2;
KMap<String, KList<Position2>> data = new KMap<>();
var multiBurst = new MultiBurst("Distance Sampler", Thread.MIN_PRIORITY);
var executor = multiBurst.burst(radius * radius);
var player = player();
Thread.ofVirtual()
.start(() -> {
int d = radius * 2;
KMap<String, AtomicInteger> data = new KMap<>();
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
var multiBurst = new MultiBurst("Region Sampler");
var executor = multiBurst.burst(radius * radius);
sender.sendMessage(C.GRAY + "Generating data...");
var loc = player.getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 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();
multiBurst.close();
J.car(c);
sender.sendMessage(C.GRAY + "Generating data...");
var loc = player().getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
int c = J.ar(() -> {
sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures");
}, 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var struct = engine.getStructureAt(x, z);
if (struct != null) {
data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z));
}
completedTasks.incrementAndGet();
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
executor.complete();
multiBurst.close();
J.car(c);
for (var key : data.keySet()) {
var list = data.get(key);
KList<Long> distances = new KList<>(list.size() - 1);
for (int i = 0; i < list.size(); i++) {
var pos = list.get(i);
double dist = Integer.MAX_VALUE;
for (var p : list) {
if (p.equals(pos)) continue;
dist = Math.min(dist, Math.sqrt(Math.pow(pos.getX() - p.getX(), 2) + Math.pow(pos.getZ() - p.getZ(), 2)));
}
if (dist == Integer.MAX_VALUE) continue;
distances.add(Math.round(dist * 16));
}
long[] array = new long[distances.size()];
for (int i = 0; i < distances.size(); i++) {
array[i] = distances.get(i);
}
Arrays.sort(array);
long min = array.length > 0 ? array[0] : 0;
long max = array.length > 0 ? array[array.length - 1] : 0;
long sum = Arrays.stream(array).sum();
long avg = array.length > 0 ? Math.round(sum / (double) array.length) : 0;
String msg = "%s: %s => min: %s/max: %s -> avg: %s".formatted(key, list.size(), min, max, avg);
sender.sendMessage(msg);
}
if (data.isEmpty()) {
sender.sendMessage(C.RED + "No data found!");
} else {
sender.sendMessage(C.GREEN + "Done!");
}
sender.sendMessage(C.GREEN + "Done!");
var loader = engine.getData().getRegionLoader();
data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%"));
});
}
@Decree(description = "Render a world map (External GUI)", aliases = "render")
@Director(description = "Render a world map (External GUI)", aliases = "render")
public void map(
@Param(name = "world", description = "The world to open the generator for", contextual = true)
World world
@@ -411,13 +340,13 @@ public class CommandStudio implements DecreeExecutor {
return;
}
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
sender().sendMessage(C.GREEN + "Opening map!");
}
@Decree(description = "Package a dimension into a compressed format", aliases = "package")
@Director(description = "Package a dimension into a compressed format", aliases = "package")
public void pkg(
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default")
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false")
boolean obfuscate,
@@ -427,9 +356,9 @@ public class CommandStudio implements DecreeExecutor {
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
}
@Decree(description = "Profiles the performance of a dimension", origin = DecreeOrigin.PLAYER)
@Director(description = "Profiles the performance of a dimension", origin = DirectorOrigin.PLAYER)
public void profile(
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
// Todo: Make this more accurate
@@ -437,6 +366,14 @@ public class CommandStudio implements DecreeExecutor {
File report = Iris.instance.getDataFile("profile.txt");
IrisProject project = new IrisProject(pack);
IrisData data = IrisData.get(pack);
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
if (activeEngine != null) {
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
} else {
IrisToolbelt.applyPregenPerformanceProfile();
}
KList<String> fileText = new KList<>();
@@ -616,7 +553,49 @@ public class CommandStudio implements DecreeExecutor {
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
}
@Decree(description = "Spawn an Iris entity", aliases = "summon", origin = DecreeOrigin.PLAYER)
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null) {
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
if (isGeneratorDimension(activeProvider, dimension)) {
return activeProvider;
}
}
}
if (!sender().isPlayer()) {
return null;
}
Player player = sender().player();
if (player == null) {
return null;
}
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
if (isGeneratorDimension(worldAccess, dimension)) {
return worldAccess;
}
return null;
}
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
return false;
}
IrisDimension engineDimension = generator.getEngine().getDimension();
if (engineDimension == null || engineDimension.getLoadKey() == null) {
return false;
}
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
}
@Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
public void spawn(
@Param(description = "The entity to spawn")
IrisEntity entity,
@@ -629,7 +608,7 @@ public class CommandStudio implements DecreeExecutor {
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
}
@Decree(description = "Teleport to the active studio world", aliases = "stp", origin = DecreeOrigin.PLAYER, sync = true)
@Director(description = "Teleport to the active studio world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
public void tpstudio() {
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world is open!");
@@ -642,13 +621,19 @@ public class CommandStudio implements DecreeExecutor {
}
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
player().teleport(Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getTarget().getWorld().spawnLocation());
player().setGameMode(GameMode.SPECTATOR);
var player = player();
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class)
.getActiveProject()
.getActiveProvider()
.getTarget()
.getWorld()
.spawnLocation()
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
}
@Decree(description = "Update your dimension projects VSCode workspace")
@Director(description = "Update your dimension projects VSCode workspace")
public void update(
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default")
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "...");
@@ -659,7 +644,7 @@ public class CommandStudio implements DecreeExecutor {
}
}
@Decree(aliases = "find-objects", description = "Get information about nearby structures")
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks")
public void objects() {
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
sender().sendMessage(C.RED + "You must be in an Iris world");
@@ -696,7 +681,7 @@ public class CommandStudio implements DecreeExecutor {
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
pw.println("MC Version: " + Bukkit.getVersion());
pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!"));
pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!"));
pw.println("Report Captured At: " + new Date());
pw.println("Chunks: (" + chunks.size() + "): ");
@@ -805,7 +790,7 @@ public class CommandStudio implements DecreeExecutor {
}
File ff = g.getData().getObjectLoader().findFile(i);
BlockVector sz = IrisObject.sampleSize(ff);
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
stop.add(i);
} catch (Throwable e) {
@@ -16,35 +16,37 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
package art.arcane.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.edit.BlockSignal;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.util.data.B;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.matter.MatterMarker;
import com.volmit.iris.util.scheduling.J;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.util.Objects;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
public class CommandWhat implements DecreeExecutor {
@Decree(description = "What is in my hand?", origin = DecreeOrigin.PLAYER)
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?")
public class CommandWhat implements DirectorExecutor {
@Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER)
public void hand() {
try {
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
@@ -65,17 +67,21 @@ public class CommandWhat implements DecreeExecutor {
}
}
@Decree(description = "What biome am i in?", origin = DecreeOrigin.PLAYER)
@Director(description = "What biome am i in?", origin = DirectorOrigin.PLAYER)
public void biome() {
try {
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + b.getDerivative().name() + ")");
Biome derivative = b.getDerivative();
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + (derivativeKey == null ? "unregistered" : derivativeKey.getKey()) + ")");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage("Non-Iris Biome: " + player().getLocation().getBlock().getBiome().name());
Biome biome = player().getLocation().getBlock().getBiome();
NamespacedKey key = resolveBiomeKey(biome);
sender().sendMessage("Non-Iris Biome: " + (key == null ? "unregistered" : key));
if (player().getLocation().getBlock().getBiome().equals(Biome.CUSTOM)) {
if (key == null || key.getKey().equals("custom")) {
try {
sender().sendMessage("Data Pack Biome: " + INMS.get().getTrueBiomeBaseKey(player().getLocation()) + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())) + ")");
} catch (Throwable ee) {
@@ -85,11 +91,11 @@ public class CommandWhat implements DecreeExecutor {
}
}
@Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER)
@Director(description = "What region am i in?", origin = DirectorOrigin.PLAYER)
public void region() {
try {
Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
IrisRegion r = engine().getRegion(chunk);
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
} catch (Throwable e) {
@@ -98,7 +104,7 @@ public class CommandWhat implements DecreeExecutor {
}
}
@Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER)
@Director(description = "What block am i looking at?", origin = DirectorOrigin.PLAYER)
public void block() {
BlockData bd;
try {
@@ -113,37 +119,37 @@ public class CommandWhat implements DecreeExecutor {
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
if (B.isStorage(bd)) {
if (BukkitBlockResolution.isStorage(bd)) {
sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)");
}
if (B.isLit(bd)) {
if (BukkitBlockResolution.isLit(bd)) {
sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)");
}
if (B.isFoliage(bd)) {
if (BukkitBlockResolution.isFoliage(bd)) {
sender().sendMessage(C.YELLOW + "* Foliage Block");
}
if (B.isDecorant(bd)) {
if (BukkitBlockResolution.isDecorant(bd)) {
sender().sendMessage(C.YELLOW + "* Decorant Block");
}
if (B.isFluid(bd)) {
if (BukkitBlockResolution.isFluid(bd)) {
sender().sendMessage(C.YELLOW + "* Fluid Block");
}
if (B.isFoliagePlantable(bd)) {
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
sender().sendMessage(C.YELLOW + "* Plantable Foliage Block");
}
if (B.isSolid(bd)) {
if (BukkitBlockResolution.isSolid(bd)) {
sender().sendMessage(C.YELLOW + "* Solid Block");
}
}
}
@Decree(description = "Show markers in chunk", origin = DecreeOrigin.PLAYER)
@Director(description = "Show markers in chunk", origin = DirectorOrigin.PLAYER)
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) {
Chunk c = player().getLocation().getChunk();
@@ -154,8 +160,8 @@ public class CommandWhat implements DecreeExecutor {
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> i.toLocation(c.getWorld())).forEach((i) -> {
J.s(() -> BlockSignal.of(i.getBlock(), 100));
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
}
@@ -166,4 +172,36 @@ public class CommandWhat implements DecreeExecutor {
sender().sendMessage(C.IRIS + "Iris worlds only.");
}
}
private NamespacedKey resolveBiomeKey(Biome biome) {
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
Object keyValue = invokeNoThrow(biome, "getKey");
if (keyValue instanceof NamespacedKey namespacedKey) {
return namespacedKey;
}
return null;
}
private Object invokeNoThrow(Biome biome, String methodName) {
if (biome == null) {
return null;
}
try {
Method method = biome.getClass().getMethod(methodName);
return method.invoke(biome);
} catch (Throwable ignored) {
return null;
}
}
}
@@ -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,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.edit;
package art.arcane.iris.core.edit;
import com.volmit.iris.util.math.M;
import art.arcane.volmlib.util.math.M;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@@ -51,10 +51,13 @@ public class BukkitBlockEditor implements BlockEditor {
return M.ms();
}
@SuppressWarnings("deprecation")
@Override
public void setBiome(int x, int z, Biome b) {
world.setBiome(x, z, b);
int minHeight = world.getMinHeight();
int maxHeight = world.getMaxHeight();
for (int y = minHeight; y < maxHeight; y++) {
world.setBiome(x, y, z, b);
}
}
@Override
@@ -67,9 +70,8 @@ public class BukkitBlockEditor implements BlockEditor {
return world.getBiome(x, y, z);
}
@SuppressWarnings("deprecation")
@Override
public Biome getBiome(int x, int z) {
return world.getBiome(x, z);
return world.getBiome(x, world.getMinHeight(), z);
}
}
@@ -0,0 +1,91 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider {
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
public static void install() {
GuiHost.set(new BukkitGuiHost());
}
@Override
public Engine findActiveEngine() {
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return null;
}
@Override
public void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
hotloadHooks.put(onHotload, listener);
Iris.instance.registerListener(listener);
}
@Override
public void unregisterHotloadHook(Runnable onHotload) {
Listener listener = hotloadHooks.remove(onHotload);
if (listener != null) {
Iris.instance.unregisterListener(listener);
}
}
@Override
public GuiOverlay overlayFor(Engine engine) {
return engine == null ? null : new BukkitVisionOverlay(engine);
}
private static final class HotloadListener implements Listener {
private final Runnable onHotload;
private HotloadListener(Runnable onHotload) {
this.onHotload = onHotload;
}
@EventHandler
public void on(IrisEngineHotloadEvent event) {
onHotload.run();
}
}
}
@@ -0,0 +1,111 @@
/*
* 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.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
public BukkitVisionOverlay(Engine engine) {
this.engine = engine;
}
@Override
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : world.getPlayers()) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
entity.getHealth(), maxHealth));
}
sink.accept(markers);
});
}
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasRealWorld()) {
return;
}
J.s(() -> {
List<Player> players = world.getPlayers();
if (players.isEmpty()) {
return;
}
Player player = players.get(0);
int xx = (int) worldX;
int zz = (int) worldZ;
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
});
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
}
@@ -16,12 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.link;
package art.arcane.iris.core.link;
import com.volmit.iris.Iris;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.Iris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
@@ -73,15 +74,15 @@ public class IrisPapiExpansion extends PlaceholderExpansion {
}
} else if (p.equalsIgnoreCase("region_name")) {
if (a != null) {
return a.getEngine().getRegion(l).getName();
return EngineBukkitOps.getRegion(a.getEngine(), l).getName();
}
} else if (p.equalsIgnoreCase("region_id")) {
if (a != null) {
return a.getEngine().getRegion(l).getLoadKey();
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadKey();
}
} else if (p.equalsIgnoreCase("region_file")) {
if (a != null) {
return a.getEngine().getRegion(l).getLoadFile().getPath();
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
}
} else if (p.equalsIgnoreCase("terrain_slope")) {
if (a != null) {
@@ -0,0 +1,310 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorSystem;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
import art.arcane.volmlib.util.director.runtime.DirectorSender;
import art.arcane.volmlib.util.director.visual.DirectorVisualCommand;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
private static final String ROOT_COMMAND = "iris";
private static final String ROOT_PERMISSION = "iris.all";
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient AtomicCache<DirectorVisualCommand> helpCache = new AtomicCache<>();
@Override
public void onEnable() {
PluginCommand command = findBukkitCommand();
if (command != null) {
command.setExecutor(this);
command.setTabCompleter(this);
} else {
registerPaperCommand();
}
J.a(this::getDirector);
}
@Override
public void onDisable() {
}
public DirectorRuntimeEngine getDirector() {
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
new CommandIris(),
null,
buildDirectorContexts(),
this::dispatchDirector,
this,
DirectorSystem.handlers
));
}
private DirectorContextRegistry buildDirectorContexts() {
DirectorContextRegistry contexts = new DirectorContextRegistry();
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
registerContextHandler(contexts, entry.getKey(), entry.getValue());
}
return contexts;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
contexts.register((Class) type, (invocation, map) -> {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
}
return null;
});
}
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
if (mode == DirectorExecutionMode.SYNC) {
J.s(runnable);
} else {
runnable.run();
}
}
@Override
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
DirectorContext.touch(new VolmitSender(sender.sender()));
}
}
@Override
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
DirectorContext.remove();
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return List.of();
}
return tabCompleteRoot(sender, alias, args);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return false;
}
executeRoot(sender, label, args);
return true;
}
void executeRoot(CommandSender sender, String label, String[] args) {
if (!sender.hasPermission(ROOT_PERMISSION)) {
sender.sendMessage("You lack the Permission '" + ROOT_PERMISSION + "'");
return;
}
J.aBukkit(() -> executeCommand(sender, label, args));
}
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
List<String> suggestions = runDirectorTab(sender, alias, args);
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
}
return suggestions;
}
private PluginCommand findBukkitCommand() {
try {
return Iris.instance.getCommand(ROOT_COMMAND);
} catch (UnsupportedOperationException ignored) {
return null;
}
}
private void registerPaperCommand() {
try {
Class<?> registrarType = Class.forName(
"art.arcane.iris.core.service.PaperCommandRegistrar",
true,
getClass().getClassLoader()
);
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
register.invoke(null, Iris.instance, this);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new IllegalStateException("Paper command registration failed", cause);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Paper command registrar is unavailable", e);
} catch (LinkageError e) {
throw new IllegalStateException("Paper command APIs are unavailable", e);
}
}
private void executeCommand(CommandSender sender, String label, String[] args) {
if (sendHelpIfRequested(sender, args)) {
playSuccessSound(sender);
return;
}
DirectorExecutionResult result = runDirector(sender, label, args);
if (result.isSuccess()) {
playSuccessSound(sender);
return;
}
playFailureSound(sender);
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
new VolmitSender(sender).sendMessage(C.RED + "Unknown Iris Command");
}
}
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
Optional<DirectorVisualCommand.HelpRequest> request = DirectorVisualCommand.resolveHelp(getHelpRoot(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
VolmitSender volmitSender = new VolmitSender(sender);
volmitSender.sendDirectorHelp(request.get().command(), request.get().page());
return true;
}
private DirectorVisualCommand getHelpRoot() {
return helpCache.aquireNastyPrint(() -> DirectorVisualCommand.createRoot(getDirector()));
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
try {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
}
}
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
DirectorContext.touch(new VolmitSender(sender));
try {
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return List.of();
} finally {
DirectorContext.remove();
}
}
private void playFailureSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
}
}
private void playSuccessSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
}
}
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
try {
player.playSound(player.getLocation(), a, av, ap);
player.playSound(player.getLocation(), b, bv, bp);
} catch (Throwable e) {
if (SOUND_LOGGED.compareAndSet(false, true)) {
try {
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
} catch (Throwable ignored) {
}
}
}
}
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
@Override
public String getName() {
return sender.getName();
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
@Override
public void sendMessage(String message) {
if (message != null && !message.trim().isEmpty()) {
sender.sendMessage(message);
}
}
}
}
@@ -16,33 +16,42 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.service;
package art.arcane.iris.core.service;
import com.volmit.iris.Iris;
import com.volmit.iris.core.edit.BlockEditor;
import com.volmit.iris.core.edit.BukkitBlockEditor;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.plugin.IrisService;
import org.bukkit.Bukkit;
import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockEditor;
import art.arcane.iris.core.edit.BukkitBlockEditor;
import art.arcane.iris.engine.framework.BlockEditAccess;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldUnloadEvent;
public class EditSVC implements IrisService {
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
private KMap<World, BlockEditor> editors;
private int updateTaskId = -1;
public static boolean deletingWorld = false;
@Override
public void onEnable() {
this.editors = new KMap<>();
Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000);
updateTaskId = J.sr(this::update, 1000);
}
@Override
public void onDisable() {
if (updateTaskId != -1) {
J.csr(updateTaskId);
updateTaskId = -1;
}
if (editors == null) {
return;
}
flushNow();
}
@@ -72,6 +81,9 @@ public class EditSVC implements IrisService {
@EventHandler
public void on(WorldUnloadEvent e) {
if (editors == null) {
return;
}
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
editors.remove(e.getWorld()).close();
}
@@ -79,6 +91,9 @@ public class EditSVC implements IrisService {
public void update() {
if (editors == null) {
return;
}
for (World i : editors.k()) {
if (M.ms() - editors.get(i).last() > 1000) {
editors.remove(i).close();
@@ -87,12 +102,18 @@ public class EditSVC implements IrisService {
}
public void flushNow() {
if (editors == null) {
return;
}
for (World i : editors.k()) {
editors.remove(i).close();
}
}
public BlockEditor open(World world) {
if (editors == null) {
editors = new KMap<>();
}
if (editors.containsKey(world)) {
return editors.get(world);
}
@@ -0,0 +1,453 @@
package art.arcane.iris.core.service;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.Looper;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import lombok.Synchronized;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineSVC implements IrisService {
private static final int TRIM_PERIOD = 2_000;
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final AtomicInteger tectonicPlates = new AtomicInteger();
private final AtomicInteger queuedTectonicPlates = new AtomicInteger();
private final AtomicInteger trimmerAlive = new AtomicInteger();
private final AtomicInteger unloaderAlive = new AtomicInteger();
private final AtomicInteger totalWorlds = new AtomicInteger();
private final AtomicDouble maxIdleDuration = new AtomicDouble();
private final AtomicDouble minIdleDuration = new AtomicDouble();
private final AtomicLong loadedChunks = new AtomicLong();
private final KMap<World, Registered> worlds = new KMap<>();
private ScheduledExecutorService service;
private Looper updateTicker;
@Override
public void onEnable() {
var settings = IrisSettings.get().getPerformance();
var engine = settings.getEngineSVC();
service = Executors.newScheduledThreadPool(0,
(engine.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(engine.getPriority()))
.name("Iris EngineSVC-", 0)
.factory());
tectonicLimit.set(settings.getTectonicPlateSize());
Bukkit.getWorlds().forEach(this::add);
setup();
}
@Override
public void onDisable() {
for (World world : worlds.keySet()) {
PlatformChunkGenerator gen = IrisToolbelt.access(world);
if (gen == null) continue;
try {
gen.close();
} catch (Throwable t) {
IrisLogging.reportError(t);
}
}
if (service != null) {
service.shutdown();
}
if (updateTicker != null) {
updateTicker.interrupt();
}
worlds.keySet().forEach(this::remove);
worlds.clear();
}
public int getQueuedTectonicPlateCount() {
return queuedTectonicPlates.get();
}
public double getAverageIdleDuration() {
double min = minIdleDuration.get();
double max = maxIdleDuration.get();
if (!Double.isFinite(min) || !Double.isFinite(max) || min < 0D || max < 0D) {
return 0D;
}
if (max < min) {
return max;
}
return (min + max) / 2D;
}
public double getBiomeCacheUsageRatio() {
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
if (preservation == null) {
return 0D;
}
double total = 0D;
int count = 0;
for (var cache : preservation.getCaches()) {
if (!(cache instanceof CachedStream2D<?>) && !(cache instanceof CachedDoubleStream2D)) {
continue;
}
double usage = cache.getUsage();
if (!Double.isFinite(usage)) {
continue;
}
total += Math.max(0D, Math.min(1D, usage));
count++;
}
if (count <= 0) {
return 0D;
}
return total / count;
}
public void engineStatus(VolmitSender sender) {
long[] sizes = new long[4];
long[] count = new long[4];
for (var cache : IrisServices.get(PreservationSVC.class).getCaches()) {
var type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
count[type]++;
}
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
sender.sendMessage(C.DARK_PURPLE + "Status:");
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (service.isShutdown() ? "Shutdown" : "Running"));
sender.sendMessage(C.DARK_PURPLE + "- Updater: " + C.LIGHT_PURPLE + (updateTicker.isAlive() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Period: " + C.LIGHT_PURPLE + Form.duration(TRIM_PERIOD));
sender.sendMessage(C.DARK_PURPLE + "- Trimmers: " + C.LIGHT_PURPLE + trimmerAlive.get());
sender.sendMessage(C.DARK_PURPLE + "- Unloaders: " + C.LIGHT_PURPLE + unloaderAlive.get());
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
sender.sendMessage(C.DARK_PURPLE + "- Limit: " + C.LIGHT_PURPLE + tectonicLimit.get());
sender.sendMessage(C.DARK_PURPLE + "- Total: " + C.LIGHT_PURPLE + tectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + queuedTectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(maxIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(minIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "Caches:");
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + sizes[0] + " (" + count[0] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + sizes[1] + " (" + count[1] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + sizes[2] + " (" + count[2] + ")");
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + sizes[3] + " (" + count[3] + ")");
sender.sendMessage(C.DARK_PURPLE + "Other:");
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + totalWorlds.get());
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + loadedChunks.get());
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
}
@EventHandler
public void onWorldUnload(WorldUnloadEvent event) {
remove(event.getWorld());
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
add(event.getWorld());
}
private void remove(World world) {
var entry = worlds.remove(world);
if (entry == null) return;
entry.close();
}
private void add(World world) {
if (world == null || worlds.containsKey(world)) return;
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) return;
worlds.put(world, new Registered(world.getName(), access));
}
private synchronized void setup() {
if (updateTicker != null && updateTicker.isAlive())
return;
updateTicker = new Looper() {
@Override
protected long loop() {
try {
for (World world : Bukkit.getWorlds()) {
add(world);
}
int queuedPlates = 0;
int totalPlates = 0;
long chunks = 0;
int unloaders = 0;
int trimmers = 0;
int iris = 0;
double maxDuration = Long.MIN_VALUE;
double minDuration = Long.MAX_VALUE;
for (var entry : worlds.entrySet()) {
var registered = entry.getValue();
if (registered.closed) continue;
iris++;
if (registered.unloaderAlive()) unloaders++;
if (registered.trimmerAlive()) trimmers++;
var engine = registered.getEngine();
if (engine == null) continue;
queuedPlates += engine.getMantle().getUnloadRegionCount();
totalPlates += engine.getMantle().getLoadedRegionCount();
chunks += entry.getKey().getLoadedChunks().length;
double duration = engine.getMantle().getAdjustedIdleDuration();
if (duration > maxDuration) maxDuration = duration;
if (duration < minDuration) minDuration = duration;
}
trimmerAlive.set(trimmers);
unloaderAlive.set(unloaders);
tectonicPlates.set(totalPlates);
queuedTectonicPlates.set(queuedPlates);
maxIdleDuration.set(maxDuration);
minIdleDuration.set(minDuration);
loadedChunks.set(chunks);
totalWorlds.set(iris);
worlds.values().forEach(Registered::update);
} catch (Throwable e) {
e.printStackTrace();
}
return 1000;
}
};
updateTicker.start();
}
private static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
static boolean shouldSkipMantleReductionForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
return maintenanceActive && !pregeneratorTargetsWorld;
}
private final class Registered {
private final String name;
private final PlatformChunkGenerator access;
private final int offset = RNG.r.nextInt(TRIM_PERIOD);
private transient ScheduledFuture<?> trimmer;
private transient ScheduledFuture<?> unloader;
private transient boolean closed;
private Registered(String name, PlatformChunkGenerator access) {
this.name = name;
this.access = access;
update();
}
private boolean unloaderAlive() {
return unloader != null && !unloader.isDone() && !unloader.isCancelled();
}
private boolean trimmerAlive() {
return trimmer != null && !trimmer.isDone() && !trimmer.isCancelled();
}
@Synchronized
private void update() {
if (closed || service == null || service.isShutdown())
return;
if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) {
trimmer = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
if (GoldenHashScanner.isScanActive()) {
return;
}
try {
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
engine.getMantle().trim(0L, 0);
} else {
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
IrisLogging.reportError(e);
IrisLogging.error("EngineSVC: Failed to trim for " + name);
e.printStackTrace();
}
}, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
if (unloader == null || unloader.isDone() || unloader.isCancelled()) {
unloader = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
if (GoldenHashScanner.isScanActive()) {
return;
}
try {
long unloadStart = System.currentTimeMillis();
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine);
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
if (heapPressure && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
if (count > 0) {
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
IrisLogging.reportError(e);
IrisLogging.error("EngineSVC: Failed to unload for " + name);
e.printStackTrace();
}
}, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
}
private int tectonicLimit() {
return tectonicLimit.get() / Math.max(worlds.size(), 1);
}
private boolean pregenTargets(Engine engine) {
if (engine == null || engine.getWorld() == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
}
private int activeTectonicLimit(Engine engine) {
int limit = tectonicLimit();
if (!pregenTargets(engine)) {
return limit;
}
return Math.max(limit, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
}
private long activeIdleDuration(Engine engine) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
@Synchronized
private void close() {
if (closed) return;
closed = true;
if (trimmer != null) {
trimmer.cancel(false);
trimmer = null;
}
if (unloader != null) {
unloader.cancel(false);
unloader = null;
}
}
@Nullable
private Engine getEngine() {
if (closed) return null;
return access.getEngine();
}
private boolean shouldReduce(Engine engine) {
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return true;
}
return pregenTargets(engine);
}
private boolean shouldSkipForMaintenance(@Nullable World world) {
if (world == null) {
return false;
}
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
if (!maintenanceActive) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
}
@@ -0,0 +1,210 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1)
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"iris-engine-metrics"
);
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
@Override
public void onEnable() {
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
Iris.verbose("Integration provider registered for Iris");
}
@Override
public void onDisable() {
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
}
@Override
public String pluginId() {
return "iris";
}
@Override
public String pluginVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public Set<IntegrationProtocolVersion> supportedProtocols() {
return SUPPORTED_PROTOCOLS;
}
@Override
public Set<String> capabilities() {
return CAPABILITIES;
}
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
return IntegrationMetricSchema.descriptors().stream()
.filter(descriptor -> descriptor.key().startsWith("iris."))
.collect(java.util.stream.Collectors.toSet());
}
@Override
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
long now = System.currentTimeMillis();
if (request == null) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"missing request",
now
);
}
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
SUPPORTED_PROTOCOLS,
request.supportedProtocols()
);
if (negotiated.isEmpty()) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"no-common-protocol",
now
);
}
negotiatedProtocol = negotiated.get();
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiatedProtocol,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
now
);
}
@Override
public IntegrationHeartbeat heartbeat() {
long now = System.currentTimeMillis();
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: metricKeys;
long now = System.currentTimeMillis();
Map<String, IntegrationMetricSample> out = new HashMap<>();
for (String key : requested) {
switch (key) {
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
default -> out.put(key, IntegrationMetricSample.unavailable(
IntegrationMetricSchema.descriptor(key),
"unsupported-key",
now
));
}
}
return out;
}
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
double idle = engineService.getAverageIdleDuration();
if (idle > 0D && Double.isFinite(idle)) {
return IntegrationMetricSample.available(descriptor, idle, now);
}
}
return IntegrationMetricSample.available(descriptor, 0D, now);
}
private IntegrationMetricSample samplePregenQueueMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
long totalQueue = 0L;
boolean hasAnySource = false;
long pregenRemaining = PregeneratorJob.chunksRemaining();
if (pregenRemaining >= 0L) {
totalQueue += pregenRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
hasAnySource = true;
}
if (!hasAnySource) {
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
}
return IntegrationMetricSample.available(descriptor, totalQueue, now);
}
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService == null) {
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
}
double ratio = engineService.getBiomeCacheUsageRatio();
if (!Double.isFinite(ratio)) {
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
}
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
}
}
@@ -0,0 +1,209 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisServerTransport;
import art.arcane.iris.core.protocol.IrisSession;
import art.arcane.iris.core.protocol.IrisSessionRegistry;
import art.arcane.iris.core.protocol.IrisVisionRequestService;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.messaging.Messenger;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.util.UUID;
public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport {
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
private static final int DIMENSION_SYNC_RETRY_TICKS = 20;
private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15;
private IrisSessionRegistry registry;
private IrisProtocolServer protocolServer;
@Override
public void onEnable() {
Messenger messenger = Bukkit.getMessenger();
messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
registry = new IrisSessionRegistry();
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
protocolServer.setEngineResolver(engineResolver);
protocolServer.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, registry));
IrisServices.register(IrisProtocolServer.class, protocolServer);
for (Player player : Bukkit.getOnlinePlayers()) {
registry.register(new IrisSession(player.getUniqueId().toString(), this));
}
}
@Override
public void onDisable() {
IrisServices.remove(IrisProtocolServer.class);
Messenger messenger = Bukkit.getMessenger();
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
IrisSessionRegistry current = registry;
if (current != null) {
for (IrisSession session : current.all()) {
current.unregister(session.id());
}
}
registry = null;
protocolServer = null;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this));
syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS);
}
@EventHandler
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
if (registry == null) {
return;
}
syncDimension(event.getPlayer(), 1);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
current.unregister(event.getPlayer().getUniqueId().toString());
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
IrisProtocolServer current = protocolServer;
if (current == null || !IrisProtocol.CHANNEL.equals(channel)) {
return;
}
current.onClientFrame(player.getUniqueId().toString(), message);
}
@Override
public void sendToClient(String sessionId, byte[] frame) {
if (sessionId == null || frame == null) {
return;
}
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return;
}
J.runEntity(player, () -> deliver(player, frame));
}
private void deliver(Player player, byte[] frame) {
if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) {
return;
}
player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame);
}
private void syncDimension(Player player, int attemptsLeft) {
IrisProtocolServer protocol = protocolServer;
IrisSessionRegistry current = registry;
if (protocol == null || current == null || !player.isOnline()) {
return;
}
String sessionId = player.getUniqueId().toString();
IrisSession session = current.get(sessionId);
if (session == null) {
return;
}
if (!session.isReady()) {
if (attemptsLeft > 1) {
J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS);
}
return;
}
World world = player.getWorld();
Engine engine = engineFor(world);
if (engine != null) {
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
return;
}
protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false);
}
private static Engine resolveEngine(String sessionId) {
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return null;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return null;
}
return engineFor(player.getWorld());
}
private static Engine engineFor(World world) {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) {
return null;
}
Engine engine = access.getEngine();
if (engine == null || engine.isClosed()) {
return null;
}
return engine;
}
private static UUID parseUuid(String sessionId) {
try {
return UUID.fromString(sessionId);
} catch (IllegalArgumentException invalid) {
return null;
}
}
private static String brand() {
return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)";
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import org.bukkit.command.CommandSender;
import java.util.Collection;
import java.util.List;
final class PaperCommandRegistrar {
private static final String ROOT_COMMAND = "iris";
private PaperCommandRegistrar() {
}
static void register(Iris plugin, CommandSVC commandService) {
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
event.registrar().register(
ROOT_COMMAND,
"Iris world generation command.",
List.of("ir", "irs"),
command(commandService)
)
);
}
static BasicCommand command(CommandSVC commandService) {
return new PaperCommand(commandService);
}
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
@Override
public void execute(CommandSourceStack source, String[] args) {
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public Collection<String> suggest(CommandSourceStack source, String[] args) {
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public boolean canUse(CommandSender sender) {
return true;
}
}
}
@@ -16,26 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.service;
package art.arcane.iris.core.service;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.edit.DustRevealer;
import com.volmit.iris.core.link.WorldEditLink;
import com.volmit.iris.core.wand.WandSelection;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.data.Cuboid;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.matter.Matter;
import com.volmit.iris.util.matter.WorldMatter;
import com.volmit.iris.util.plugin.IrisService;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.SR;
import com.volmit.iris.util.scheduling.jobs.Job;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.edit.DustRevealer;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.wand.WandSelection;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.util.project.matter.WorldMatter;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.SR;
import art.arcane.iris.util.common.scheduling.jobs.Job;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -51,12 +58,11 @@ import org.bukkit.util.Vector;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import static com.volmit.iris.util.data.registry.Particles.CRIT_MAGIC;
import static com.volmit.iris.util.data.registry.Particles.REDSTONE;
import static art.arcane.iris.util.common.data.registry.Particles.CRIT_MAGIC;
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
public class WandSVC implements IrisService {
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
@@ -90,6 +96,7 @@ public class WandSVC implements IrisService {
int total = c.getSizeX() * c.getSizeY() * c.getSizeZ();
var latch = new CountDownLatch(1);
var holder = Iris.tickets.getHolder(p.getWorld());
new Job() {
private int i;
private Chunk chunk;
@@ -108,7 +115,7 @@ public class WandSVC implements IrisService {
while (time > M.ms()) {
if (!it.hasNext()) {
if (chunk != null) {
chunk.removePluginChunkTicket(Iris.instance);
holder.removeTicket(chunk);
chunk = null;
}
@@ -122,9 +129,10 @@ public class WandSVC implements IrisService {
var bChunk = b.getChunk();
if (chunk == null) {
chunk = bChunk;
chunk.addPluginChunkTicket(Iris.instance);
holder.addTicket(chunk);
} else if (chunk != bChunk) {
chunk.removePluginChunkTicket(Iris.instance);
holder.removeTicket(chunk);
holder.addTicket(bChunk);
chunk = bChunk;
}
@@ -204,7 +212,9 @@ public class WandSVC implements IrisService {
if (f.length != 2) return null;
String[] g = f[0].split("\\Q,\\E");
if (g.length != 3) return null;
return new Location(Bukkit.getWorld(f[1]), Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
World world = WorldIdentity.resolve(f[1]).orElse(null);
if (world == null) return null;
return new Location(world, Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
} catch (Throwable e) {
Iris.reportError(e);
return null;
@@ -222,7 +232,7 @@ public class WandSVC implements IrisService {
return "<#>";
}
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + loc.getWorld().getName();
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld());
}
/**
@@ -526,7 +536,7 @@ public class WandSVC implements IrisService {
Location[] f = getCuboidFromItem(item);
Location other = left ? f[1] : f[0];
if (other != null && !other.getWorld().getName().equals(a.getWorld().getName())) {
if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) {
other = null;
}
@@ -16,18 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.wand;
package art.arcane.iris.core.wand;
import com.volmit.iris.util.data.Cuboid;
import com.volmit.iris.util.math.M;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.volmlib.util.math.M;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.awt.*;
import java.awt.Color;
import static com.volmit.iris.util.data.registry.Particles.REDSTONE;
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
public class WandSelection {
private final Cuboid c;
@@ -0,0 +1,10 @@
name: ${name}
version: ${version}
main: ${main}
bootstrapper: ${bootstrapper}
folia-supported: true
api-version: '${apiVersion}'
load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
@@ -1,11 +1,12 @@
name: ${name}
version: ${version}
main: ${main}
load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
name: ${name}
version: ${version}
main: ${main}
folia-supported: true
load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
commands:
iris:
aliases: [ ir, irs ]
api-version: '${apiVersion}'
api-version: '${apiVersion}'
@@ -0,0 +1,199 @@
package art.arcane.iris;
import io.papermc.paper.datapack.Datapack;
import io.papermc.paper.datapack.DatapackRegistrar;
import io.papermc.paper.datapack.DatapackSource;
import io.papermc.paper.datapack.DiscoveredDatapack;
import io.papermc.paper.plugin.configuration.PluginMeta;
import net.kyori.adventure.text.Component;
import org.bukkit.FeatureFlag;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisBootstrapTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void discoverPackAutoEnablesAtFixedTop() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.ACCEPT);
DiscoveredDatapack discovered = IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath());
assertSame(registrar.discoveredDatapack, discovered);
assertEquals(datapackDirectory.toPath(), registrar.discoveredPath);
assertEquals(IrisBootstrap.PACK_ID, registrar.discoveredId);
assertTrue(registrar.configurer.autoEnableOnServerStart);
assertTrue(registrar.configurer.fixedPosition);
assertEquals(Datapack.Position.TOP, registrar.configurer.position);
}
@Test
public void rejectedDiscoveryReportsDatapackPath() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.REJECT);
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
);
assertTrue(failure.getMessage().contains(datapackDirectory.toPath().toString()));
}
@Test
public void discoveryIoFailurePropagates() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.FAIL);
IOException failure = assertThrows(
IOException.class,
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
);
assertEquals("discovery failed", failure.getMessage());
}
private enum DiscoveryBehavior {
ACCEPT,
REJECT,
FAIL
}
private static final class RecordingConfigurer implements DatapackRegistrar.Configurer {
private boolean autoEnableOnServerStart;
private boolean fixedPosition;
private Datapack.Position position;
@Override
public DatapackRegistrar.Configurer title(Component title) {
return this;
}
@Override
public DatapackRegistrar.Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart) {
this.autoEnableOnServerStart = autoEnableOnServerStart;
return this;
}
@Override
public DatapackRegistrar.Configurer position(boolean fixed, Datapack.Position position) {
this.fixedPosition = fixed;
this.position = position;
return this;
}
}
private static final class RecordingRegistrar implements DatapackRegistrar {
private final DiscoveryBehavior behavior;
private final RecordingConfigurer configurer;
private final DiscoveredDatapack discoveredDatapack;
private Path discoveredPath;
private String discoveredId;
private RecordingRegistrar(DiscoveryBehavior behavior) {
this.behavior = behavior;
this.configurer = new RecordingConfigurer();
this.discoveredDatapack = new StubDiscoveredDatapack();
}
@Override
public boolean hasPackDiscovered(String name) {
return false;
}
@Override
public DiscoveredDatapack getDiscoveredPack(String name) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeDiscoveredPack(String name) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, DiscoveredDatapack> getDiscoveredPacks() {
return Collections.emptyMap();
}
@Override
public DiscoveredDatapack discoverPack(URI uri, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
@Override
public DiscoveredDatapack discoverPack(Path path, String id, Consumer<Configurer> configurer) throws IOException {
this.discoveredPath = path;
this.discoveredId = id;
if (behavior == DiscoveryBehavior.FAIL) {
throw new IOException("discovery failed");
}
configurer.accept(this.configurer);
return behavior == DiscoveryBehavior.ACCEPT ? discoveredDatapack : null;
}
@Override
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
@Override
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
}
private static final class StubDiscoveredDatapack implements DiscoveredDatapack {
@Override
public String getName() {
return IrisBootstrap.PACK_ID;
}
@Override
public Component getTitle() {
return Component.text(IrisBootstrap.PACK_ID);
}
@Override
public Component getDescription() {
return Component.empty();
}
@Override
public boolean isRequired() {
return true;
}
@Override
public Datapack.Compatibility getCompatibility() {
return Datapack.Compatibility.COMPATIBLE;
}
@Override
public Set<FeatureFlag> getRequiredFeatures() {
return Collections.emptySet();
}
@Override
public DatapackSource getSource() {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisDiagnosticsTest {
@Test
public void reportErrorWithContextPrintsFullStacktrace() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
try {
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
} finally {
System.setErr(originalErr);
}
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Runtime world creation failed."));
assertTrue(text.contains("IllegalStateException"));
assertTrue(text.contains("IllegalArgumentException"));
assertTrue(text.contains("inner"));
}
@Test
public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception {
Path root = Files.createTempDirectory("iris-splash");
try {
Path validPack = root.resolve("overworld");
Files.createDirectories(validPack.resolve("dimensions"));
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
Files.createDirectories(root.resolve("datapack-imports"));
Path brokenPack = root.resolve("broken");
Files.createDirectories(brokenPack.resolve("dimensions"));
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
List<IrisSplashPackScanner.SplashPackMetadata> packs;
try {
packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError);
} finally {
System.setErr(originalErr);
}
assertEquals(1, packs.size());
assertEquals("overworld", packs.get(0).name());
assertEquals("4000", packs.get(0).version());
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
assertTrue(text.contains("Json"));
} finally {
Files.walk(root)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}
@@ -0,0 +1,59 @@
package art.arcane.iris;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoadOrder;
import org.junit.Test;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest {
@Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
String metadata;
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/paper-plugin.yml")) {
assertNotNull(stream);
metadata = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
}
@Test
public void bukkitMetadataRetainsStartupCommandAndAliases() throws Exception {
PluginDescriptionFile metadata;
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/plugin.yml")) {
assertNotNull(stream);
metadata = new PluginDescriptionFile(stream);
}
assertEquals(Iris.class.getName(), metadata.getMain());
assertEquals(PluginLoadOrder.STARTUP, metadata.getLoad());
Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
}
@Test
public void processedResourcesContainBothMetadataFormats() throws Exception {
URL paperMetadata = PaperPluginMetadataTest.class.getResource("/paper-plugin.yml");
assertNotNull(paperMetadata);
assertEquals("file", paperMetadata.getProtocol());
Path bukkitMetadata = Path.of(paperMetadata.toURI()).resolveSibling("plugin.yml");
assertTrue(Files.isRegularFile(bukkitMetadata));
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.core;
import art.arcane.iris.core.commands.CommandStudio;
import art.arcane.iris.core.tools.IrisCreator;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
public class StudioRuntimeCleanupTest {
@Test
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
assertFalse(found);
}
@Test
public void studioCommandNoLongerExposesProfilecache() {
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("profilecache"));
assertFalse(found);
}
@Test
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
.map(method -> method.getName().toLowerCase())
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
assertFalse(found);
}
@Test
public void noisemapPrebakePipelineClassIsRemoved() {
try {
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
} catch (ClassNotFoundException ignored) {
return;
}
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisEngineSVCTest {
@Test
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
}
@Test
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
}
@Test
public void noMaintenanceNeverSkipsReduction() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.core.service;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class PaperCommandRegistrarTest {
@Test
public void paperCommandDelegatesExecutionAndSuggestions() {
RecordingCommandService service = new RecordingCommandService();
BasicCommand command = PaperCommandRegistrar.command(service);
CommandSender sender = sender();
CommandSourceStack source = new TestCommandSourceStack(sender);
String[] arguments = {"create", "world"};
command.execute(source, arguments);
assertSame(sender, service.sender);
assertEquals("iris", service.label);
assertArrayEquals(arguments, service.arguments);
assertEquals(List.of("first", "second"), command.suggest(source, new String[]{"cr"}));
assertTrue(command.canUse(sender));
}
private static CommandSender sender() {
return (CommandSender) Proxy.newProxyInstance(
CommandSender.class.getClassLoader(),
new Class<?>[]{CommandSender.class},
(proxy, method, arguments) -> primitiveDefault(method.getReturnType())
);
}
private static Object primitiveDefault(Class<?> type) {
if (type == boolean.class) {
return false;
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0F;
}
if (type == double.class) {
return 0D;
}
if (type == char.class) {
return '\0';
}
return null;
}
private static final class RecordingCommandService extends CommandSVC {
private CommandSender sender;
private String label;
private String[] arguments;
@Override
void executeRoot(CommandSender sender, String label, String[] args) {
this.sender = sender;
this.label = label;
this.arguments = args;
}
@Override
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
return List.of("first", "second");
}
}
private record TestCommandSourceStack(CommandSender sender) implements CommandSourceStack {
@Override
public Location getLocation() {
return null;
}
@Override
public CommandSender getSender() {
return sender;
}
@Override
public Entity getExecutor() {
return null;
}
@Override
public CommandSourceStack withLocation(Location location) {
return this;
}
@Override
public CommandSourceStack withExecutor(Entity executor) {
return this;
}
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.client;
@FunctionalInterface
public interface ClientPacketSink {
void send(byte[] frame);
}
@@ -0,0 +1,161 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.spi.protocol.ProtocolException;
import net.minecraft.resources.Identifier;
public final class IrisClient {
public static final Identifier HUD_ELEMENT_ID = Identifier.fromNamespaceAndPath("irisworldgen", "pregen_hud");
public static final Identifier KEYBIND_CATEGORY_ID = Identifier.fromNamespaceAndPath("irisworldgen", "iris");
public static final String KEYBIND_TOGGLE_HUD = "key.irisworldgen.toggle_pregen_hud";
public static final String KEYBIND_OPEN_MAP = "key.irisworldgen.open_vision_map";
public static final String KEYBIND_TOGGLE_WHAT = "key.irisworldgen.toggle_what_overlay";
private static final IrisClientSession SESSION = new IrisClientSession();
private static final IrisClientPregenState PREGEN = new IrisClientPregenState();
private static final IrisClientDimension DIMENSION = new IrisClientDimension();
private static final IrisClientTileCache TILES = new IrisClientTileCache(IrisClient::sendFrame, System::currentTimeMillis);
private static final IrisClientCursor CURSOR = new IrisClientCursor(IrisClient::sendFrame, System::currentTimeMillis);
private static final IrisClientMarkers MARKERS = new IrisClientMarkers();
private static final IrisClientRegionMap REGIONS = new IrisClientRegionMap();
private static final IrisClientToasts TOASTS = new IrisClientToasts();
private static volatile ClientPacketSink sink;
private static volatile boolean hudVisible = true;
private static volatile boolean whatVisible = false;
private IrisClient() {
}
public static IrisClientSession session() {
return SESSION;
}
public static IrisClientPregenState pregen() {
return PREGEN;
}
public static IrisClientDimension dimension() {
return DIMENSION;
}
public static IrisClientTileCache tiles() {
return TILES;
}
public static IrisClientCursor cursor() {
return CURSOR;
}
public static IrisClientMarkers markers() {
return MARKERS;
}
public static IrisClientRegionMap regions() {
return REGIONS;
}
public static IrisClientToasts toasts() {
return TOASTS;
}
public static boolean hudVisible() {
return hudVisible;
}
public static void toggleHud() {
hudVisible = !hudVisible;
}
public static boolean whatVisible() {
return whatVisible;
}
public static void toggleWhat() {
whatVisible = !whatVisible;
}
public static boolean visionAvailable() {
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_VISION) != 0L;
}
public static boolean cursorAvailable() {
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_CURSOR) != 0L;
}
public static void bindSender(ClientPacketSink boundSink) {
sink = boundSink;
SESSION.bind(boundSink);
}
public static void sendFrame(byte[] frame) {
ClientPacketSink activeSink = sink;
if (activeSink != null) {
activeSink.send(frame);
}
}
public static void onWorldJoin() {
SESSION.sendHello();
}
public static void onDisconnect() {
SESSION.reset();
PREGEN.clear();
DIMENSION.clear();
TILES.clear();
CURSOR.clear();
MARKERS.clear();
REGIONS.clear();
TOASTS.clear();
}
public static void onInbound(byte[] frame) {
if (frame == null) {
return;
}
IrisMessage message;
try {
message = IrisMessageCodec.decode(frame);
} catch (ProtocolException rejected) {
return;
}
if (message == null) {
return;
}
route(message);
}
private static void route(IrisMessage message) {
switch (message) {
case IrisMessage.ServerHello serverHello -> SESSION.onServerHello(serverHello);
case IrisMessage.PregenProgress progress -> PREGEN.onProgress(progress);
case IrisMessage.PregenEnd end -> onPregenEnd(end.jobId());
case IrisMessage.DimensionStatus status -> onDimensionStatus(status);
case IrisMessage.CursorInfo cursorInfo -> CURSOR.onCursorInfo(cursorInfo);
case IrisMessage.VisionTile visionTile -> TILES.onVisionTile(visionTile);
case IrisMessage.VisionMarkers visionMarkers -> MARKERS.onMarkers(visionMarkers);
case IrisMessage.PregenRegionDelta delta -> REGIONS.onDelta(delta, PREGEN.activeJobId());
case IrisMessage.StudioHotload hotload -> TOASTS.enqueueHotload(hotload.packKey(), hotload.changedFiles(), hotload.failed(), hotload.message());
case IrisMessage.Toast toast -> TOASTS.enqueue(toast.kind(), toast.title(), toast.body());
default -> {
}
}
}
private static void onPregenEnd(long jobId) {
PREGEN.onEnd(jobId);
REGIONS.onEnd(jobId);
}
private static void onDimensionStatus(IrisMessage.DimensionStatus status) {
boolean changed = DIMENSION.onDimensionStatus(status);
if (changed) {
TILES.clear();
MARKERS.clear();
CURSOR.clear();
REGIONS.clear();
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import java.util.function.LongSupplier;
public final class IrisClientCursor {
private static final long MIN_REQUEST_INTERVAL_MILLIS = 500L;
private final ClientPacketSink sink;
private final LongSupplier clock;
private volatile IrisMessage.CursorInfo latest;
private int lastRequestedX;
private int lastRequestedZ;
private boolean requested;
private long lastRequestMillis;
public IrisClientCursor(ClientPacketSink sink, LongSupplier clock) {
this.sink = sink;
this.clock = clock;
this.latest = null;
this.lastRequestedX = 0;
this.lastRequestedZ = 0;
this.requested = false;
this.lastRequestMillis = 0L;
}
public synchronized void requestFor(int blockX, int blockZ) {
if (requested && blockX == lastRequestedX && blockZ == lastRequestedZ) {
return;
}
long now = clock.getAsLong();
if (now - lastRequestMillis < MIN_REQUEST_INTERVAL_MILLIS) {
return;
}
sink.send(IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(blockX, blockZ)));
lastRequestedX = blockX;
lastRequestedZ = blockZ;
requested = true;
lastRequestMillis = now;
}
public void onCursorInfo(IrisMessage.CursorInfo info) {
latest = info;
}
public IrisMessage.CursorInfo latest() {
return latest;
}
public synchronized void clear() {
latest = null;
requested = false;
lastRequestMillis = 0L;
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
public final class IrisClientDimension {
private volatile IrisMessage.DimensionStatus status;
public boolean onDimensionStatus(IrisMessage.DimensionStatus incoming) {
IrisMessage.DimensionStatus previous = status;
status = incoming;
if (previous == null) {
return true;
}
return previous.irisWorld() != incoming.irisWorld() || !previous.dimensionKey().equals(incoming.dimensionKey());
}
public IrisMessage.DimensionStatus status() {
return status;
}
public boolean irisWorld() {
IrisMessage.DimensionStatus current = status;
return current != null && current.irisWorld();
}
public void clear() {
status = null;
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.client;
import net.minecraft.client.gui.GuiGraphicsExtractor;
public final class IrisClientHud {
private IrisClientHud() {
}
public static void render(GuiGraphicsExtractor graphics) {
IrisToastPresenter.pump();
IrisPregenHud.render(graphics);
IrisWhatOverlay.render(graphics);
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.client;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
public final class IrisClientKeybinds {
private static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(IrisClient.KEYBIND_CATEGORY_ID);
public static final KeyMapping TOGGLE_HUD = new KeyMapping(IrisClient.KEYBIND_TOGGLE_HUD, GLFW.GLFW_KEY_H, CATEGORY);
public static final KeyMapping OPEN_MAP = new KeyMapping(IrisClient.KEYBIND_OPEN_MAP, GLFW.GLFW_KEY_M, CATEGORY);
public static final KeyMapping TOGGLE_WHAT = new KeyMapping(IrisClient.KEYBIND_TOGGLE_WHAT, GLFW.GLFW_KEY_J, CATEGORY);
private IrisClientKeybinds() {
}
public static KeyMapping.Category category() {
return CATEGORY;
}
public static void pollToggle() {
while (TOGGLE_HUD.consumeClick()) {
IrisClient.toggleHud();
}
while (TOGGLE_WHAT.consumeClick()) {
IrisClient.toggleWhat();
}
while (OPEN_MAP.consumeClick()) {
openVisionScreen();
}
}
private static void openVisionScreen() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.level == null || minecraft.player == null) {
return;
}
minecraft.setScreenAndShow(new IrisVisionScreen());
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientMarkers {
private final Map<IrisTileKey, List<IrisMessage.VisionMarkers.Marker>> byTile;
public IrisClientMarkers() {
this.byTile = new ConcurrentHashMap<>();
}
public void onMarkers(IrisMessage.VisionMarkers markers) {
byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers()));
}
public List<IrisMessage.VisionMarkers.Marker> forTile(IrisTileKey key) {
return byTile.get(key);
}
public void clear() {
byTile.clear();
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientPregenState {
private final ConcurrentHashMap<Long, IrisMessage.PregenProgress> jobs;
private volatile Long activeJobId;
public IrisClientPregenState() {
this.jobs = new ConcurrentHashMap<>();
this.activeJobId = null;
}
public void onProgress(IrisMessage.PregenProgress progress) {
jobs.put(progress.jobId(), progress);
activeJobId = progress.jobId();
}
public void onEnd(long jobId) {
jobs.remove(jobId);
Long current = activeJobId;
if (current != null && current == jobId) {
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
}
}
public IrisMessage.PregenProgress active() {
Long current = activeJobId;
if (current == null) {
return null;
}
return jobs.get(current);
}
public Long activeJobId() {
return activeJobId;
}
public void clear() {
jobs.clear();
activeJobId = null;
}
}
@@ -0,0 +1,118 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientRegionMap {
private static final int MAX_REGIONS = 4096;
private final ConcurrentHashMap<Long, Integer> states;
private volatile boolean tracking;
private volatile long jobId;
private volatile Bounds bounds;
public IrisClientRegionMap() {
this.states = new ConcurrentHashMap<>();
this.tracking = false;
this.jobId = 0L;
this.bounds = null;
}
public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) {
if (activeJobId == null) {
return;
}
long active = activeJobId;
if (delta.jobId() != active) {
return;
}
if (!tracking || jobId != delta.jobId()) {
resetTo(delta.jobId());
}
record(delta.regionX(), delta.regionZ(), delta.state());
}
public void onEnd(long endedJobId) {
if (tracking && jobId == endedJobId) {
clear();
}
}
public boolean hasData() {
return tracking && !states.isEmpty();
}
public Bounds bounds() {
return bounds;
}
public void forEachCell(CellConsumer consumer) {
for (Map.Entry<Long, Integer> entry : states.entrySet()) {
long packed = entry.getKey();
consumer.accept(regionX(packed), regionZ(packed), entry.getValue());
}
}
public void clear() {
states.clear();
bounds = null;
tracking = false;
jobId = 0L;
}
private void resetTo(long newJobId) {
states.clear();
bounds = null;
jobId = newJobId;
tracking = true;
}
private void record(int regionX, int regionZ, int state) {
long packed = pack(regionX, regionZ);
if (!states.containsKey(packed)) {
if (states.size() >= MAX_REGIONS) {
return;
}
Bounds current = bounds;
bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ);
}
states.put(packed, state);
}
private static long pack(int regionX, int regionZ) {
return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL);
}
private static int regionX(long packed) {
return (int) (packed >> 32);
}
private static int regionZ(long packed) {
return (int) packed;
}
@FunctionalInterface
public interface CellConsumer {
void accept(int regionX, int regionZ, int state);
}
public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
public Bounds union(int regionX, int regionZ) {
return new Bounds(
Math.min(minRegionX, regionX),
Math.min(minRegionZ, regionZ),
Math.max(maxRegionX, regionX),
Math.max(maxRegionZ, regionZ));
}
public int regionsWide() {
return maxRegionX - minRegionX + 1;
}
public int regionsTall() {
return maxRegionZ - minRegionZ + 1;
}
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
public final class IrisClientSession {
private static final long CLIENT_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR | IrisProtocol.CAPABILITY_STUDIO;
private volatile State state;
private volatile long serverCapabilities;
private volatile boolean irisActive;
private volatile String serverBrand;
private volatile ClientPacketSink sink;
public IrisClientSession() {
this.state = State.IDLE;
this.serverCapabilities = 0L;
this.irisActive = false;
this.serverBrand = "";
this.sink = null;
}
public void bind(ClientPacketSink boundSink) {
this.sink = boundSink;
}
public State state() {
return state;
}
public boolean isReady() {
return state == State.READY;
}
public boolean irisActive() {
return irisActive;
}
public long serverCapabilities() {
return serverCapabilities;
}
public String serverBrand() {
return serverBrand;
}
public void sendHello() {
ClientPacketSink activeSink = sink;
if (activeSink == null) {
return;
}
byte[] frame = IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, CLIENT_CAPABILITIES));
state = State.AWAITING_HELLO;
activeSink.send(frame);
}
public void onServerHello(IrisMessage.ServerHello hello) {
this.serverCapabilities = hello.capabilities();
this.irisActive = hello.irisActive();
this.serverBrand = hello.serverBrand();
this.state = State.READY;
}
public void reset() {
this.state = State.IDLE;
this.serverCapabilities = 0L;
this.irisActive = false;
this.serverBrand = "";
}
public enum State {
IDLE,
AWAITING_HELLO,
READY
}
}
@@ -0,0 +1,113 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.LongSupplier;
public final class IrisClientTileCache {
private static final int MAX_CACHED_TILES = 256;
private static final long REQUEST_RETRY_MILLIS = 3000L;
private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND;
private final ClientPacketSink sink;
private final LongSupplier clock;
private final IrisTileAssembler assembler;
private final LinkedHashMap<IrisTileKey, IrisTileImage> cache;
private final Map<IrisTileKey, Long> pending;
private final Deque<IrisTileKey> queue;
private final Set<IrisTileKey> queued;
private long windowStartMillis;
private int sentInWindow;
public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) {
this.sink = sink;
this.clock = clock;
this.assembler = new IrisTileAssembler();
this.cache = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, IrisTileImage> eldest) {
return size() > MAX_CACHED_TILES;
}
};
this.pending = new HashMap<>();
this.queue = new ArrayDeque<>();
this.queued = new HashSet<>();
this.windowStartMillis = 0L;
this.sentInWindow = 0;
}
public synchronized void onVisionTile(IrisMessage.VisionTile tile) {
IrisTileImage image = assembler.add(tile);
if (image == null) {
return;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
cache.put(key, image);
pending.remove(key);
queued.remove(key);
}
public synchronized IrisTileImage get(IrisTileKey key) {
return cache.get(key);
}
public synchronized void resetRequestQueue() {
queue.clear();
queued.clear();
}
public synchronized void request(IrisTileKey key) {
if (cache.containsKey(key)) {
return;
}
long now = clock.getAsLong();
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
return;
}
if (queued.add(key)) {
queue.addLast(key);
}
}
public synchronized void pump() {
long now = clock.getAsLong();
if (now - windowStartMillis >= 1000L) {
windowStartMillis = now;
sentInWindow = 0;
}
while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) {
IrisTileKey key = queue.pollFirst();
queued.remove(key);
if (cache.containsKey(key)) {
continue;
}
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
continue;
}
sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom())));
pending.put(key, now);
sentInWindow++;
}
}
public synchronized void clear() {
assembler.clear();
cache.clear();
pending.clear();
queue.clear();
queued.clear();
sentInWindow = 0;
windowStartMillis = 0L;
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.ArrayDeque;
public final class IrisClientToasts {
private static final int MAX_PENDING = 4;
private final ArrayDeque<Pending> pending;
public IrisClientToasts() {
this.pending = new ArrayDeque<>();
}
public void enqueue(int kind, String title, String body) {
Pending entry = new Pending(kind, normalize(title), normalize(body));
synchronized (pending) {
while (pending.size() >= MAX_PENDING) {
pending.pollFirst();
}
pending.addLast(entry);
}
}
public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) {
int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS;
enqueue(kind, "Studio Hotload", hotloadBody(packKey, changedFiles, failed, message));
}
public Pending poll() {
synchronized (pending) {
return pending.pollFirst();
}
}
public void clear() {
synchronized (pending) {
pending.clear();
}
}
private static String normalize(String value) {
return value == null ? "" : value;
}
private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) {
StringBuilder builder = new StringBuilder();
String pack = normalize(packKey);
if (!pack.isEmpty()) {
builder.append(pack);
}
if (changedFiles != 0) {
append(builder, changedFiles + (changedFiles == 1 ? " file" : " files"));
}
String text = normalize(message);
if (!text.isEmpty()) {
append(builder, text);
}
if (builder.isEmpty()) {
return failed ? "reload failed" : "reloaded";
}
return builder.toString();
}
private static void append(StringBuilder builder, String part) {
if (!builder.isEmpty()) {
builder.append(" ");
}
builder.append(part);
}
public record Pending(int kind, String title, String body) {
}
}
@@ -0,0 +1,153 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
public final class IrisPregenHud {
private static final int PANEL_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFD7D7D7;
private static final int BAR_BACK_COLOR = 0xFF2B2B2B;
private static final int BAR_RUNNING_COLOR = 0xFF66BB6A;
private static final int PAUSED_COLOR = 0xFFFFD54F;
private static final int GRID_BACK_COLOR = 0xFF161616;
private static final int CELL_PENDING_COLOR = 0xFF3A3A3A;
private static final int CELL_GENERATING_COLOR = 0xFFFFD54F;
private static final int CELL_DONE_COLOR = 0xFF66BB6A;
private static final int ORIGIN_X = 6;
private static final int ORIGIN_Y = 6;
private static final int PADDING = 4;
private static final int ROW_GAP = 2;
private static final int BAR_HEIGHT = 6;
private static final int MIN_WIDTH = 130;
private static final int MINIMAP_MAX_PX = 64;
private static final int MINIMAP_MAX_CELL = 6;
private static final int MINIMAP_GAP = 4;
private IrisPregenHud() {
}
public static void render(GuiGraphicsExtractor graphics) {
if (!IrisClient.hudVisible()) {
return;
}
IrisMessage.PregenProgress progress = IrisClient.pregen().active();
if (progress == null) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.player == null) {
return;
}
Font font = minecraft.font;
boolean paused = progress.state() == IrisMessage.PregenProgress.STATE_PAUSED;
double percent = progress.chunksTotal() > 0L
? clampPercent((double) progress.chunksDone() / (double) progress.chunksTotal() * 100.0D)
: 0.0D;
String title = "Iris Pregen";
String stats = String.format("%,d / %,d (%.1f%%)", progress.chunksDone(), progress.chunksTotal(), percent);
String tail = paused ? "PAUSED" : rateAndEta(progress);
int accent = paused ? PAUSED_COLOR : BAR_RUNNING_COLOR;
int lineHeight = font.lineHeight;
int contentWidth = Math.max(MIN_WIDTH, Math.max(font.width(title), Math.max(font.width(stats), font.width(tail))));
int contentHeight = lineHeight * 3 + ROW_GAP * 3 + BAR_HEIGHT;
IrisClientRegionMap regionMap = IrisClient.regions();
IrisClientRegionMap.Bounds bounds = regionMap.hasData() ? regionMap.bounds() : null;
boolean showMap = bounds != null;
int cellPx = 0;
int gridWidth = 0;
int gridHeight = 0;
if (showMap) {
int span = Math.max(bounds.regionsWide(), bounds.regionsTall());
cellPx = Math.max(1, Math.min(MINIMAP_MAX_CELL, MINIMAP_MAX_PX / span));
gridWidth = Math.min(MINIMAP_MAX_PX, bounds.regionsWide() * cellPx);
gridHeight = Math.min(MINIMAP_MAX_PX, bounds.regionsTall() * cellPx);
}
int panelWidth = showMap ? Math.max(contentWidth, gridWidth) : contentWidth;
int panelHeight = showMap ? contentHeight + MINIMAP_GAP + gridHeight : contentHeight;
graphics.fill(ORIGIN_X - PADDING, ORIGIN_Y - PADDING, ORIGIN_X + panelWidth + PADDING, ORIGIN_Y + panelHeight + PADDING, PANEL_COLOR);
int cursorY = ORIGIN_Y;
graphics.text(font, title, ORIGIN_X, cursorY, TITLE_COLOR);
cursorY += lineHeight + ROW_GAP;
graphics.text(font, stats, ORIGIN_X, cursorY, TEXT_COLOR);
cursorY += lineHeight + ROW_GAP;
int fillWidth = (int) Math.round(contentWidth * (percent / 100.0D));
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + contentWidth, cursorY + BAR_HEIGHT, BAR_BACK_COLOR);
if (fillWidth > 0) {
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + fillWidth, cursorY + BAR_HEIGHT, accent);
}
cursorY += BAR_HEIGHT + ROW_GAP;
graphics.text(font, tail, ORIGIN_X, cursorY, paused ? PAUSED_COLOR : MUTED_COLOR);
if (showMap) {
renderMinimap(graphics, regionMap, bounds, cellPx, gridWidth, gridHeight, ORIGIN_Y + contentHeight + MINIMAP_GAP);
}
}
private static void renderMinimap(GuiGraphicsExtractor graphics, IrisClientRegionMap regionMap, IrisClientRegionMap.Bounds bounds, int cellPx, int gridWidth, int gridHeight, int gridTop) {
int mapLeft = ORIGIN_X;
int mapRight = mapLeft + gridWidth;
int mapBottom = gridTop + gridHeight;
graphics.fill(mapLeft, gridTop, mapRight, mapBottom, GRID_BACK_COLOR);
int minRegionX = bounds.minRegionX();
int minRegionZ = bounds.minRegionZ();
regionMap.forEachCell((regionX, regionZ, state) -> {
int px = mapLeft + (regionX - minRegionX) * cellPx;
int py = gridTop + (regionZ - minRegionZ) * cellPx;
if (px + cellPx > mapRight || py + cellPx > mapBottom) {
return;
}
graphics.fill(px, py, px + cellPx, py + cellPx, cellColor(state));
});
}
private static int cellColor(int state) {
return switch (state) {
case IrisMessage.PregenRegionDelta.STATE_DONE -> CELL_DONE_COLOR;
case IrisMessage.PregenRegionDelta.STATE_GENERATING -> CELL_GENERATING_COLOR;
default -> CELL_PENDING_COLOR;
};
}
private static String rateAndEta(IrisMessage.PregenProgress progress) {
String rate = String.format("%,.0f/s", progress.chunksPerSecond());
if (progress.etaMillis() > 0L) {
return rate + " ETA " + formatDuration(progress.etaMillis());
}
return rate;
}
private static String formatDuration(long etaMillis) {
long totalSeconds = etaMillis / 1000L;
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
if (hours > 0L) {
return hours + "h " + minutes + "m";
}
if (minutes > 0L) {
return minutes + "m " + seconds + "s";
}
return seconds + "s";
}
private static double clampPercent(double value) {
if (value < 0.0D) {
return 0.0D;
}
if (value > 100.0D) {
return 100.0D;
}
return value;
}
}
@@ -0,0 +1,86 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.HashMap;
import java.util.Map;
final class IrisTileAssembler {
private final Map<IrisTileKey, Partial> partials;
IrisTileAssembler() {
this.partials = new HashMap<>();
}
IrisTileImage add(IrisMessage.VisionTile tile) {
int chunkCount = tile.chunkCount();
int chunkIndex = tile.chunkIndex();
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
return null;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
Partial partial = partials.get(key);
if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) {
if (partial != null && tile.sequence() < partial.sequence()) {
return null;
}
partial = new Partial(tile.sequence(), chunkCount);
partials.put(key, partial);
} else if (tile.sequence() < partial.sequence()) {
return null;
}
if (!partial.accept(chunkIndex, tile.data())) {
return null;
}
partials.remove(key);
return IrisTileCodec.decode(partial.concat());
}
void clear() {
partials.clear();
}
private static final class Partial {
private final int sequence;
private final int chunkCount;
private final byte[][] chunks;
private int received;
Partial(int sequence, int chunkCount) {
this.sequence = sequence;
this.chunkCount = chunkCount;
this.chunks = new byte[chunkCount][];
this.received = 0;
}
int sequence() {
return sequence;
}
int chunkCount() {
return chunkCount;
}
boolean accept(int chunkIndex, byte[] data) {
if (chunks[chunkIndex] == null) {
chunks[chunkIndex] = data;
received++;
}
return received >= chunkCount;
}
byte[] concat() {
int total = 0;
for (byte[] chunk : chunks) {
total += chunk.length;
}
byte[] blob = new byte[total];
int position = 0;
for (byte[] chunk : chunks) {
System.arraycopy(chunk, 0, blob, position, chunk.length);
position += chunk.length;
}
return blob;
}
}
}
@@ -0,0 +1,99 @@
package art.arcane.iris.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
public final class IrisTileCodec {
public static final int MODE_RAW_RGB = 0;
public static final int MODE_PALETTE = 1;
private static final int MAX_DIMENSION = 512;
private static final int OPAQUE = 0xFF000000;
private IrisTileCodec() {
}
public static IrisTileImage decode(byte[] deflatedBlob) {
if (deflatedBlob == null || deflatedBlob.length == 0) {
return null;
}
byte[] raw = inflate(deflatedBlob);
if (raw == null) {
return null;
}
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw))) {
int width = in.readInt();
int height = in.readInt();
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
return null;
}
int mode = in.readUnsignedByte();
int[] argb = new int[width * height];
return switch (mode) {
case MODE_PALETTE -> decodePalette(in, width, height, argb);
case MODE_RAW_RGB -> decodeRaw(in, width, height, argb);
default -> null;
};
} catch (IOException failure) {
return null;
}
}
private static IrisTileImage decodePalette(DataInputStream in, int width, int height, int[] argb) throws IOException {
int paletteSize = in.readInt();
if (paletteSize <= 0 || paletteSize > 256) {
return null;
}
int[] palette = new int[paletteSize];
for (int index = 0; index < paletteSize; index++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
palette[index] = OPAQUE | red << 16 | green << 8 | blue;
}
for (int pixel = 0; pixel < argb.length; pixel++) {
int paletteIndex = in.readUnsignedByte();
if (paletteIndex >= paletteSize) {
return null;
}
argb[pixel] = palette[paletteIndex];
}
return new IrisTileImage(width, height, argb);
}
private static IrisTileImage decodeRaw(DataInputStream in, int width, int height, int[] argb) throws IOException {
for (int pixel = 0; pixel < argb.length; pixel++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
argb[pixel] = OPAQUE | red << 16 | green << 8 | blue;
}
return new IrisTileImage(width, height, argb);
}
private static byte[] inflate(byte[] input) {
Inflater inflater = new Inflater();
inflater.setInput(input);
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2));
byte[] buffer = new byte[8192];
try {
while (!inflater.finished()) {
int produced = inflater.inflate(buffer);
if (produced == 0) {
if (inflater.finished() || inflater.needsInput() || inflater.needsDictionary()) {
break;
}
}
out.write(buffer, 0, produced);
}
} catch (DataFormatException malformed) {
return null;
} finally {
inflater.end();
}
return out.toByteArray();
}
}
@@ -0,0 +1,4 @@
package art.arcane.iris.client;
public record IrisTileImage(int width, int height, int[] argb) {
}
@@ -0,0 +1,4 @@
package art.arcane.iris.client;
public record IrisTileKey(int tileX, int tileZ, int zoom) {
}
@@ -0,0 +1,35 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastManager;
import net.minecraft.network.chat.Component;
public final class IrisToastPresenter {
private IrisToastPresenter() {
}
public static void pump() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.gui == null) {
return;
}
ToastManager manager = minecraft.gui.toastManager();
IrisClientToasts toasts = IrisClient.toasts();
IrisClientToasts.Pending next = toasts.poll();
while (next != null) {
SystemToast.addOrUpdate(manager, tokenFor(next.kind()), Component.literal(next.title()), Component.literal(next.body()));
next = toasts.poll();
}
}
private static SystemToast.SystemToastId tokenFor(int kind) {
return switch (kind) {
case IrisMessage.Toast.KIND_SUCCESS -> SystemToast.SystemToastId.WORLD_BACKUP;
case IrisMessage.Toast.KIND_WARNING -> SystemToast.SystemToastId.UNSECURE_SERVER_WARNING;
case IrisMessage.Toast.KIND_ERROR -> SystemToast.SystemToastId.PACK_LOAD_FAILURE;
default -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION;
};
}
}
@@ -0,0 +1,328 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class IrisVisionScreen extends Screen {
private static final int TILE_PIXELS = 128;
private static final int MIN_ZOOM = 0;
private static final int MAX_ZOOM = 8;
private static final int DEFAULT_ZOOM = 2;
private static final int MAX_TEXTURES = 220;
private static final int BACKGROUND_COLOR = 0xF00B0E14;
private static final int PLACEHOLDER_COLOR = 0xFF161A22;
private static final int GRID_COLOR = 0x33FFFFFF;
private static final int HEADER_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFB9B9B9;
private static final int PLAYER_FILL_COLOR = 0xFFFFDD33;
private static final int PLAYER_BORDER_COLOR = 0xFF101010;
private static final int MARKER_COLOR = 0xFF4FC3F7;
private static final int MARKER_LABEL_BG = 0xE0101010;
private final Map<IrisTileKey, TileTexture> textures;
private double centerBlockX;
private double centerBlockZ;
private int zoom;
private boolean initialized;
private String renderedDimensionKey;
public IrisVisionScreen() {
super(Component.literal("Iris Vision"));
this.textures = new LinkedHashMap<>(64, 0.75f, true);
this.centerBlockX = 0.0D;
this.centerBlockZ = 0.0D;
this.zoom = DEFAULT_ZOOM;
this.initialized = false;
this.renderedDimensionKey = null;
}
@Override
protected void init() {
if (!initialized) {
centerOnPlayer();
initialized = true;
}
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
graphics.fill(0, 0, width, height, BACKGROUND_COLOR);
IrisClientSession session = IrisClient.session();
if (!session.isReady()) {
drawCentered(graphics, "Connecting to Iris server...", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", "not connected");
return;
}
IrisMessage.DimensionStatus status = IrisClient.dimension().status();
if (!IrisClient.visionAvailable() || status == null) {
drawCentered(graphics, "Not an Iris world", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", status == null ? "no dimension data" : label(status));
return;
}
syncWorld(status);
renderTiles(graphics, mouseX, mouseY, status);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
return true;
}
@Override
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
int blocksPerPixel = 1 << zoom;
centerBlockX -= dragX * blocksPerPixel;
centerBlockZ -= dragY * blocksPerPixel;
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (scrollY == 0.0D) {
return false;
}
int previousBlocksPerPixel = 1 << zoom;
double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel;
double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel;
int updated = zoom + (scrollY > 0.0D ? -1 : 1);
zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM));
int newBlocksPerPixel = 1 << zoom;
centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel;
centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel;
return true;
}
@Override
public void removed() {
releaseTextures();
super.removed();
}
private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) {
int blocksPerPixel = 1 << zoom;
int tileSpanBlocks = TILE_PIXELS * blocksPerPixel;
int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel);
int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel);
int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1;
int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1;
int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1;
int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1;
int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks);
int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks);
IrisClientTileCache cache = IrisClient.tiles();
cache.resetRequestQueue();
List<IrisTileKey> missing = new ArrayList<>();
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
int screenX = originX + tileX * TILE_PIXELS;
int screenY = originY + tileZ * TILE_PIXELS;
IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom);
IrisTileImage image = cache.get(key);
if (image == null) {
graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR);
missing.add(key);
continue;
}
Identifier texture = ensureTexture(key, image);
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS);
}
}
requestMissing(cache, missing, centerTileX, centerTileZ);
cache.pump();
evictTextures();
drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel);
drawPlayer(graphics, blocksPerPixel);
drawHeader(graphics, "Iris Vision", label(status) + " zoom " + zoom + " x" + (long) centerBlockX + " z" + (long) centerBlockZ);
drawFooter(graphics, "Drag to pan Scroll to zoom Esc to close");
}
private void requestMissing(IrisClientTileCache cache, List<IrisTileKey> missing, int centerTileX, int centerTileZ) {
missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ)));
for (IrisTileKey key : missing) {
cache.request(key);
}
}
private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) {
IrisClientMarkers markers = IrisClient.markers();
String hoverLabel = null;
int hoverX = 0;
int hoverY = 0;
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
List<IrisMessage.VisionMarkers.Marker> tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom));
if (tileMarkers == null) {
continue;
}
for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) {
int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel);
int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel);
graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR);
if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) {
hoverLabel = marker.label();
hoverX = screenX;
hoverY = screenY;
}
}
}
}
if (hoverLabel != null) {
int labelWidth = font.width(hoverLabel);
graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG);
graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR);
}
}
private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel);
int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel);
graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR);
graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR);
}
private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) {
int lineHeight = font.lineHeight;
int headerHeight = lineHeight + 8;
graphics.fill(0, 0, width, headerHeight, HEADER_COLOR);
graphics.text(font, title, 8, 4, TITLE_COLOR);
graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR);
}
private void drawFooter(GuiGraphicsExtractor graphics, String hint) {
int lineHeight = font.lineHeight;
int footerTop = height - lineHeight - 8;
graphics.fill(0, footerTop, width, height, HEADER_COLOR);
graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR);
}
private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) {
graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color);
}
private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) {
TileTexture existing = textures.get(key);
if (existing != null) {
return existing.id();
}
int tileWidth = image.width();
int tileHeight = image.height();
int[] argb = image.argb();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false);
for (int y = 0; y < tileHeight; y++) {
int row = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x]));
}
}
DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage);
Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key));
Minecraft.getInstance().getTextureManager().register(id, texture);
textures.put(key, new TileTexture(id, texture));
return id;
}
private void evictTextures() {
if (textures.size() <= MAX_TEXTURES) {
return;
}
TextureManager manager = Minecraft.getInstance().getTextureManager();
Iterator<Map.Entry<IrisTileKey, TileTexture>> iterator = textures.entrySet().iterator();
while (textures.size() > MAX_TEXTURES && iterator.hasNext()) {
Map.Entry<IrisTileKey, TileTexture> entry = iterator.next();
manager.release(entry.getValue().id());
iterator.remove();
}
}
private void releaseTextures() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft != null) {
TextureManager manager = minecraft.getTextureManager();
for (TileTexture texture : textures.values()) {
manager.release(texture.id());
}
}
textures.clear();
}
private void syncWorld(IrisMessage.DimensionStatus status) {
if (renderedDimensionKey == null) {
renderedDimensionKey = status.dimensionKey();
return;
}
if (!renderedDimensionKey.equals(status.dimensionKey())) {
renderedDimensionKey = status.dimensionKey();
releaseTextures();
centerOnPlayer();
}
}
private void centerOnPlayer() {
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
centerBlockX = player.getBlockX();
centerBlockZ = player.getBlockZ();
}
}
private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) {
long deltaX = (long) key.tileX() - centerTileX;
long deltaZ = (long) key.tileZ() - centerTileZ;
return deltaX * deltaX + deltaZ * deltaZ;
}
private static int toAbgr(int argb) {
int alpha = argb >>> 24 & 0xFF;
int red = argb >> 16 & 0xFF;
int green = argb >> 8 & 0xFF;
int blue = argb & 0xFF;
return alpha << 24 | blue << 16 | green << 8 | red;
}
private static String texturePath(IrisTileKey key) {
return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom();
}
private static String safe(int value) {
return value < 0 ? "m" + (-(long) value) : Integer.toString(value);
}
private static String label(IrisMessage.DimensionStatus status) {
String pack = status.packKey() == null || status.packKey().isEmpty() ? "" : " pack " + status.packKey();
return status.dimensionKey() + pack;
}
private record TileTexture(Identifier id, DynamicTexture texture) {
}
}
@@ -0,0 +1,85 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.ArrayList;
import java.util.List;
public final class IrisWhatOverlay {
private static final int PANEL_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFC7C7C7;
private static final int PADDING = 4;
private static final int ROW_GAP = 2;
private static final int CURSOR_OFFSET = 12;
private IrisWhatOverlay() {
}
public static void render(GuiGraphicsExtractor graphics) {
if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.player == null) {
return;
}
LocalPlayer player = minecraft.player;
int blockX = player.getBlockX();
int blockZ = player.getBlockZ();
HitResult hit = minecraft.hitResult;
if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) {
BlockPos pos = blockHit.getBlockPos();
blockX = pos.getX();
blockZ = pos.getZ();
}
IrisClient.cursor().requestFor(blockX, blockZ);
IrisMessage.CursorInfo info = IrisClient.cursor().latest();
List<String> lines = new ArrayList<>();
if (info == null) {
lines.add("querying " + blockX + ", " + blockZ + "...");
} else {
lines.add("Biome: " + display(info.biomeKey()));
lines.add("Region: " + display(info.regionKey()));
if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) {
lines.add("Cave: " + display(info.caveBiomeKey()));
}
lines.add("Height: " + info.height() + " (" + info.blockX() + ", " + info.blockZ() + ")");
}
draw(graphics, minecraft.font, lines);
}
private static void draw(GuiGraphicsExtractor graphics, Font font, List<String> lines) {
int lineHeight = font.lineHeight;
int contentWidth = font.width("Iris What");
for (String line : lines) {
contentWidth = Math.max(contentWidth, font.width(line));
}
int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET;
int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET;
int rows = lines.size() + 1;
int contentHeight = lineHeight * rows + ROW_GAP * rows;
graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR);
int cursorY = originY;
graphics.text(font, "Iris What", originX, cursorY, TITLE_COLOR);
cursorY += lineHeight + ROW_GAP;
for (String line : lines) {
graphics.text(font, line, originX, cursorY, line.startsWith("Height") ? MUTED_COLOR : TEXT_COLOR);
cursorY += lineHeight + ROW_GAP;
}
}
private static String display(String key) {
return key == null || key.isEmpty() ? "-" : key;
}
}
+221
View File
@@ -0,0 +1,221 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
id 'net.fabricmc.fabric-loom' version '1.17.11'
alias(libs.plugins.shadow)
}
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
group = 'art.arcane'
version = irisVersion
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
}
}
test {
java {
srcDir '../modded-common/src/test/java'
}
}
}
repositories {
mavenCentral()
maven {
name = 'FabricMC'
url = uri('https://maven.fabricmc.net/')
}
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') }
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
}
configurations {
bundle {
canBeConsumed = false
canBeResolved = true
}
devBundle {
canBeConsumed = false
canBeResolved = true
}
}
configurations.named('bundle').configure {
exclude(group: 'com.google.code.gson')
exclude(group: 'com.google.guava')
exclude(group: 'commons-io')
exclude(group: 'org.apache.commons', module: 'commons-lang3')
exclude(group: 'it.unimi.dsi', module: 'fastutil')
exclude(group: 'org.slf4j')
exclude(group: 'org.apache.logging.log4j')
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
configurations.create('jij') {
transitive = true
}
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2')
List<Object> fabricApi = [
libs.fabricApi.base,
libs.fabricApi.registrySync,
libs.fabricApi.resourceLoader,
libs.fabricApi.lifecycleEvents,
libs.fabricApi.commandApi,
libs.fabricApi.eventsInteraction,
libs.fabricApi.networking,
libs.fabricApi.rendering,
libs.fabricApi.keyMapping
]
fabricApi.each { Object notation ->
add('jij', notation)
add('implementation', notation)
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
}
List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
volmLibCoordinate,
libs.paralithic,
libs.lru,
libs.kotlin.stdlib,
libs.kotlin.coroutines,
libs.commons.lang,
libs.commons.math3,
libs.caffeine,
libs.lz4,
libs.zip,
libs.sentry,
libs.oshi,
libs.byteBuddy.core,
libs.byteBuddy.agent
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
}
loom {
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
runs {
server {
String parity = providers.gradleProperty('irisParity').getOrNull()
if (parity != null) {
property('iris.parity', parity)
}
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
if (parityGolden != null) {
property('iris.parity.golden', parityGolden)
}
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
if (parityDeep != null) {
property('iris.parity.deep', parityDeep)
}
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
if (worldCheck != null) {
property('iris.worldcheck', worldCheck)
}
vmArg('-Xmx8G')
programArgs('nogui')
}
}
}
processResources {
inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion)
filesMatching('fabric.mod.json') {
expand('version': project.version, 'minecraftVersion': minecraftVersion)
}
}
tasks.named('shadowJar', ShadowJar).configure {
doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar"))
}
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
configurations = [project.configurations.named('bundle').get()]
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
from(project.configurations.named('jij')) {
into('META-INF/jars')
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
}
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
}
+5
View File
@@ -0,0 +1,5 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.configuration-cache=false
+95
View File
@@ -0,0 +1,95 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
maven {
name = 'FabricMC'
url = uri('https://maven.fabricmc.net/')
}
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'iris-fabric'
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../gradle/libs.versions.toml'))
}
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
includeBuild('../..') {
dependencySubstitution {
substitute(module('art.arcane:core')).using(project(':core'))
substitute(module('art.arcane:spi')).using(project(':spi'))
}
}
@@ -0,0 +1,48 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedForcedDatapack;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Set;
public final class FabricForcedDatapackSources {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricForcedDatapackSources() {
}
public static void attach(PackRepository repository) {
if (repository == null) {
return;
}
try {
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
repository.sources = merged;
} catch (Throwable e) {
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
}
}
}
@@ -0,0 +1,73 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.server.MinecraftServer;
import java.io.File;
import java.nio.file.Path;
public final class FabricModdedLoader implements ModdedLoader {
@Override
public String platformName() {
return "fabric";
}
@Override
public String minecraftVersion() {
return FabricLoader.getInstance().getModContainer("minecraft")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public String modVersion() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
Object instance = FabricLoader.getInstance().getGameInstance();
return instance instanceof MinecraftServer server ? server : null;
}
@Override
public boolean clientEnvironment() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
}
@Override
public Path configDir() {
return FabricLoader.getInstance().getConfigDir();
}
@Override
public File modJar() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst())
.map((Path p) -> p.toFile())
.orElse(null);
}
}
@@ -0,0 +1,54 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
public final class FabricProtocolNetworking {
private FabricProtocolNetworking() {
}
public static void install() {
PayloadTypeRegistry.clientboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
ServerPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
(payload, context) -> ModdedProtocolHandler.onInbound(context.player(), payload.data()));
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ModdedProtocolHandler.onPlayerJoin(handler.player));
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> ModdedProtocolHandler.onPlayerDisconnect(handler.player));
ModdedProtocolHandler.bindChannel(new FabricProtocolChannel());
}
private static final class FabricProtocolChannel implements ModdedProtocolChannel {
@Override
public boolean canReceive(ServerPlayer player) {
return ServerPlayNetworking.canSend(player, ModdedIrisPayload.TYPE);
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
ServerPlayNetworking.send(player, payload);
}
}
}
@@ -0,0 +1,65 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
public final class IrisFabricBootstrap implements ModInitializer {
@Override
public void onInitialize() {
ModdedEngineBootstrap.bootCommon(new FabricModdedLoader(), "Fabric",
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
FabricProtocolNetworking.install();
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> {
ModdedEngineBootstrap.tick(server);
ModdedWandService.serverTick(server);
});
}
}
@@ -0,0 +1,50 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.client.IrisClient;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.iris.client.IrisClientKeybinds;
import art.arcane.iris.modded.ModdedIrisPayload;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
public final class IrisFabricClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
(payload, context) -> IrisClient.onInbound(payload.data()));
IrisClient.bindSender(frame -> {
if (ClientPlayNetworking.canSend(ModdedIrisPayload.TYPE)) {
ClientPlayNetworking.send(new ModdedIrisPayload(frame));
}
});
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> IrisClient.onWorldJoin());
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> IrisClient.onDisconnect());
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_HUD);
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.OPEN_MAP);
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_WHAT);
HudElementRegistry.addLast(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics));
ClientTickEvents.END_CLIENT_TICK.register(client -> IrisClientKeybinds.pollToggle());
}
}
@@ -0,0 +1,35 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.modded.ModdedDeathLoot;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntity.class)
public class LivingEntityMixin {
@Inject(method = "die", at = @At("TAIL"))
private void iris$dropIrisDeathLoot(DamageSource damageSource, CallbackInfo info) {
ModdedDeathLoot.handle((LivingEntity) (Object) this);
}
}
@@ -0,0 +1,36 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.fabric.FabricForcedDatapackSources;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerPacksSource.class)
public class ServerPacksSourceMixin {
@Inject(method = "createPackRepository(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/server/packs/repository/PackRepository;", at = @At("RETURN"))
private static void iris$addForcedDatapackSource(LevelStorageSource.LevelStorageAccess storage, CallbackInfoReturnable<PackRepository> info) {
FabricForcedDatapackSources.attach(info.getReturnValue());
}
}
@@ -0,0 +1,36 @@
{
"schemaVersion": 1,
"id": "irisworldgen",
"version": "${version}",
"name": "Iris",
"description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)",
"authors": ["Arcane Arts (Volmit Software)"],
"contact": {
"sources": "https://github.com/VolmitSoftware/Iris"
},
"license": "GPL-3.0",
"environment": "*",
"accessWidener": "irisworldgen.accesswidener",
"mixins": ["irisworldgen.mixins.json"],
"entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
"client": ["art.arcane.iris.fabric.IrisFabricClient"]
},
"jars": [
{ "file": "META-INF/jars/fabric-api-base.jar" },
{ "file": "META-INF/jars/fabric-registry-sync-v0.jar" },
{ "file": "META-INF/jars/fabric-resource-loader-v1.jar" },
{ "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" },
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
{ "file": "META-INF/jars/fabric-command-api-v2.jar" },
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" },
{ "file": "META-INF/jars/fabric-rendering-v1.jar" },
{ "file": "META-INF/jars/fabric-transitive-access-wideners-v1.jar" },
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }
],
"depends": {
"fabricloader": ">=0.19.3",
"minecraft": "~${minecraftVersion}",
"java": ">=25"
}
}
@@ -0,0 +1,6 @@
accessWidener v2 official
accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map;
accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor;
accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;
accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
@@ -0,0 +1,13 @@
{
"required": true,
"minVersion": "0.8",
"package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"LivingEntityMixin",
"ServerPacksSourceMixin"
],
"injectors": {
"defaultRequire": 1
}
}
+245
View File
@@ -0,0 +1,245 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
id 'net.minecraftforge.gradle' version '7.0.27'
alias(libs.plugins.shadow)
}
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.0'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) {
return loaderVersion.substring(coordinatePrefix.length())
}
return loaderVersion
}
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
group = 'art.arcane'
version = irisVersion
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
}
}
}
repositories {
minecraft.mavenizer(it)
maven fg.forgeMaven
maven fg.minecraftLibsMaven
mavenCentral()
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') }
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
}
configurations {
bundle {
canBeConsumed = false
canBeResolved = true
}
devBundle {
canBeConsumed = false
canBeResolved = true
}
runBundle {
canBeConsumed = false
canBeResolved = true
}
commonsLangRaw {
canBeConsumed = false
canBeResolved = true
transitive = false
}
}
configurations.named('bundle').configure {
exclude(group: 'com.google.code.gson')
exclude(group: 'com.google.guava')
exclude(group: 'commons-io')
exclude(group: 'org.apache.commons', module: 'commons-lang3')
exclude(group: 'it.unimi.dsi', module: 'fastutil')
exclude(group: 'org.slf4j')
exclude(group: 'org.apache.logging.log4j')
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.named('runBundle').configure {
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
['compileClasspath', 'runtimeClasspath'].each { String name ->
configurations.named(name).configure {
resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') {
selectHighestVersion()
}
}
}
dependencies {
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
}
List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
volmLibCoordinate,
libs.paralithic,
libs.lru,
libs.kotlin.stdlib,
libs.kotlin.coroutines,
libs.commons.math3,
libs.caffeine,
libs.lz4,
libs.zip,
libs.sentry,
libs.oshi,
libs.byteBuddy.core,
libs.byteBuddy.agent
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
add('runBundle', notation)
}
add('bundle', libs.commons.lang)
add('devBundle', libs.commons.lang)
add('commonsLangRaw', libs.commons.lang)
}
TaskProvider<Jar> sanitizedCommonsLang = tasks.register('sanitizedCommonsLang', Jar) {
archiveBaseName.set('commons-lang-sanitized')
destinationDirectory.set(layout.buildDirectory.dir('sanitized'))
from({ zipTree(configurations.named('commonsLangRaw').get().singleFile) })
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
dependencies {
add('runBundle', files(sanitizedCommonsLang))
}
minecraft {
accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg'))
runs {
register('server') {
workingDir = layout.projectDirectory.dir('run')
String parity = providers.gradleProperty('irisParity').getOrNull()
if (parity != null) {
systemProperty('iris.parity', parity)
}
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
if (parityGolden != null) {
systemProperty('iris.parity.golden', parityGolden)
}
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
if (parityDeep != null) {
systemProperty('iris.parity.deep', parityDeep)
}
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
if (worldCheck != null) {
systemProperty('iris.worldcheck', worldCheck)
}
jvmArgs('-Xmx8G')
args('--nogui')
}
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
}
processResources {
inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion)
filesMatching('META-INF/mods.toml') {
expand('version': project.version, 'minecraftVersion': minecraftVersion)
}
}
tasks.named('shadowJar', ShadowJar).configure {
doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
}
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**')
exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**')
exclude('org/checkerframework/**')
exclude('org/jetbrains/annotations/**')
exclude('org/intellij/lang/**')
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
}
+6
View File
@@ -0,0 +1,6 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.configuration-cache=false
net.minecraftforge.gradle.merge-source-sets=true
+95
View File
@@ -0,0 +1,95 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
gradlePluginPortal()
maven {
name = 'MinecraftForge'
url = uri('https://maven.minecraftforge.net/')
}
mavenCentral()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'iris-forge'
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../gradle/libs.versions.toml'))
}
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
includeBuild('../..') {
dependencySubstitution {
substitute(module('art.arcane:core')).using(project(':core'))
substitute(module('art.arcane:spi')).using(project(':spi'))
}
}
@@ -1,6 +1,6 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,27 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.util.scheduling;
package art.arcane.iris.forge;
public class Switch {
private volatile boolean b;
import art.arcane.iris.client.IrisClient;
import net.minecraftforge.event.network.CustomPayloadEvent;
/**
* Defaulted off
*/
public Switch() {
b = false;
public final class ForgeClientProtocol {
private ForgeClientProtocol() {
}
public void flip() {
b = true;
}
public boolean isFlipped() {
return b;
}
public void reset() {
b = false;
public static void handleInbound(CustomPayloadEvent.Context context, byte[] data) {
context.enqueueWork(() -> IrisClient.onInbound(data));
}
}
@@ -0,0 +1,71 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.server.ServerLifecycleHooks;
import java.io.File;
import java.nio.file.Path;
public final class ForgeModdedLoader implements ModdedLoader {
@Override
public String platformName() {
return "forge";
}
@Override
public String minecraftVersion() {
return FMLLoader.versionInfo().mcVersion();
}
@Override
public String modVersion() {
return ModList.getModContainerById("irisworldgen")
.map((net.minecraftforge.fml.ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
return ServerLifecycleHooks.getCurrentServer();
}
@Override
public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient();
}
@Override
public Path configDir() {
return FMLPaths.CONFIGDIR.get();
}
@Override
public File modJar() {
IModFileInfo info = ModList.getModFileById("irisworldgen");
return info == null ? null : info.getFile().getFilePath().toFile();
}
}
@@ -0,0 +1,86 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import art.arcane.iris.spi.protocol.IrisProtocol;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.event.network.CustomPayloadEvent;
import net.minecraftforge.network.Channel;
import net.minecraftforge.network.ChannelBuilder;
import net.minecraftforge.network.NetworkProtocol;
import net.minecraftforge.network.PacketDistributor;
public final class ForgeProtocolNetworking {
private static volatile Channel<CustomPacketPayload> channel;
private ForgeProtocolNetworking() {
}
public static void register() {
Channel<CustomPacketPayload> boundChannel = ChannelBuilder.named(Identifier.parse(IrisProtocol.CHANNEL))
.optional()
.payloadChannel()
.protocol(NetworkProtocol.PLAY)
.bidirectional()
.add(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, ForgeProtocolNetworking::onPayload)
.build();
channel = boundChannel;
ModdedProtocolHandler.bindChannel(new ForgeProtocolChannel(boundChannel));
}
public static Channel<CustomPacketPayload> channel() {
return channel;
}
private static void onPayload(ModdedIrisPayload payload, CustomPayloadEvent.Context context) {
context.setPacketHandled(true);
if (context.isClientSide()) {
ForgeClientProtocol.handleInbound(context, payload.data());
return;
}
ServerPlayer player = context.getSender();
if (player == null) {
return;
}
ModdedProtocolHandler.onInbound(player, payload.data());
}
private static final class ForgeProtocolChannel implements ModdedProtocolChannel {
private final Channel<CustomPacketPayload> channel;
private ForgeProtocolChannel(Channel<CustomPacketPayload> channel) {
this.channel = channel;
}
@Override
public boolean canReceive(ServerPlayer player) {
return true;
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
channel.send(payload, PacketDistributor.PLAYER.with(player));
}
}
}
@@ -0,0 +1,93 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDeathLoot;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedProtocolHandler;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.event.AddPackFindersEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.registries.DeferredRegister;
import java.util.function.Predicate;
@Mod("irisworldgen")
public final class IrisForgeBootstrap {
public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
ModdedEngineBootstrap.bootCommon(new ForgeModdedLoader(), "Forge " + FMLLoader.versionInfo().forgeVersion(), () -> {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(context.getModBusGroup());
});
ForgeProtocolNetworking.register();
if (FMLEnvironment.dist == Dist.CLIENT) {
IrisForgeClient.init();
}
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerJoin(player);
}
});
PlayerEvent.PlayerLoggedOutEvent.BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerDisconnect(player);
}
});
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
}
});
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
LivingDropsEvent.BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity()));
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> {
ModdedEngineBootstrap.tick(event.server());
ModdedWandService.serverTick(event.server());
});
}
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.client.IrisClient;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.iris.client.IrisClientKeybinds;
import art.arcane.iris.modded.ModdedIrisPayload;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.network.Channel;
import net.minecraftforge.network.PacketDistributor;
public final class IrisForgeClient {
private IrisForgeClient() {
}
public static void init() {
IrisClient.bindSender(IrisForgeClient::sendToServer);
AddGuiOverlayLayersEvent.BUS.addListener((AddGuiOverlayLayersEvent event) ->
event.getLayeredDraw().add(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics)));
RegisterKeyMappingsEvent.BUS.addListener((RegisterKeyMappingsEvent event) -> {
event.register(IrisClientKeybinds.TOGGLE_HUD);
event.register(IrisClientKeybinds.OPEN_MAP);
event.register(IrisClientKeybinds.TOGGLE_WHAT);
});
ClientPlayerNetworkEvent.LoggingIn.BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin());
ClientPlayerNetworkEvent.LoggingOut.BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect());
InputEvent.Key.BUS.addListener((InputEvent.Key event) -> IrisClientKeybinds.pollToggle());
}
private static void sendToServer(byte[] frame) {
Channel<CustomPacketPayload> channel = ForgeProtocolNetworking.channel();
if (channel == null) {
return;
}
channel.send(new ModdedIrisPayload(frame), PacketDistributor.SERVER.noArg());
}
}
@@ -0,0 +1,3 @@
public net.minecraft.server.MinecraftServer levels
public net.minecraft.server.MinecraftServer executor
public net.minecraft.server.MinecraftServer storageSource
@@ -0,0 +1,24 @@
modLoader = "javafml"
loaderVersion = "[65,)"
license = "GPL-3.0"
[[mods]]
modId = "irisworldgen"
version = "${version}"
displayName = "Iris"
description = "Iris World Generation Engine (Forge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)"
authors = "Arcane Arts (Volmit Software)"
[[dependencies.irisworldgen]]
modId = "forge"
mandatory = true
versionRange = "[65,)"
ordering = "NONE"
side = "BOTH"
[[dependencies.irisworldgen]]
modId = "minecraft"
mandatory = true
versionRange = "[${minecraftVersion}]"
ordering = "NONE"
side = "BOTH"
@@ -0,0 +1,10 @@
{
"pack": {
"description": "Iris World Generation Engine resources",
"max_format": 101,
"min_format": [
101,
1
]
}
}
@@ -0,0 +1,159 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
final class InitialSpawnQueue {
private static final long DEFAULT_MAX_AGE_NANOS = TimeUnit.MINUTES.toNanos(5);
private final int capacity;
private final long maxAgeNanos;
private final LongSupplier nanoTime;
private final ArrayDeque<Long> queue;
private final Map<Long, Long> pending;
private final Set<Long> queued;
private boolean closed;
InitialSpawnQueue(int capacity) {
this(capacity, DEFAULT_MAX_AGE_NANOS, System::nanoTime);
}
InitialSpawnQueue(int capacity, long maxAgeNanos, LongSupplier nanoTime) {
if (capacity < 1) {
throw new IllegalArgumentException("capacity must be positive");
}
if (maxAgeNanos < 1L) {
throw new IllegalArgumentException("maxAgeNanos must be positive");
}
this.capacity = capacity;
this.maxAgeNanos = maxAgeNanos;
this.nanoTime = nanoTime;
this.queue = new ArrayDeque<>(Math.min(capacity, 256));
this.pending = new HashMap<>();
this.queued = new HashSet<>();
}
synchronized boolean offer(long key) {
if (closed || pending.containsKey(key)) {
return false;
}
if (pending.size() >= capacity) {
expire(nanoTime.getAsLong());
}
if (pending.size() >= capacity) {
return false;
}
pending.put(key, nanoTime.getAsLong());
queued.add(key);
queue.addLast(key);
return true;
}
synchronized int batchSize(int limit) {
if (closed || limit <= 0) {
return 0;
}
return Math.min(limit, queue.size());
}
synchronized Long poll() {
long now = nanoTime.getAsLong();
Long key;
while ((key = queue.pollFirst()) != null) {
queued.remove(key);
Long offeredAt = pending.get(key);
if (offeredAt == null) {
continue;
}
if (expired(offeredAt, now)) {
pending.remove(key);
continue;
}
return key;
}
return null;
}
synchronized void retry(long key) {
Long offeredAt = pending.get(key);
if (closed || offeredAt == null || expired(offeredAt, nanoTime.getAsLong())) {
pending.remove(key);
queued.remove(key);
return;
}
if (!queued.add(key)) {
return;
}
queue.addLast(key);
}
synchronized void complete(long key) {
pending.remove(key);
if (queued.remove(key)) {
queue.removeFirstOccurrence(key);
}
}
synchronized boolean isEmpty() {
return queue.isEmpty();
}
synchronized int size() {
return pending.size();
}
synchronized void clear() {
queue.clear();
pending.clear();
queued.clear();
}
synchronized void close() {
closed = true;
clear();
}
private void expire(long now) {
Set<Long> expired = new HashSet<>();
for (Map.Entry<Long, Long> entry : pending.entrySet()) {
if (expired(entry.getValue(), now)) {
expired.add(entry.getKey());
}
}
if (expired.isEmpty()) {
return;
}
for (Long key : expired) {
pending.remove(key);
queued.remove(key);
}
queue.removeIf(expired::contains);
}
private boolean expired(long offeredAt, long now) {
return now - offeredAt >= maxAgeNanos;
}
}
@@ -0,0 +1,584 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
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.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
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.templatesystem.StructureTemplateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.EnumSet;
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.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public final class IrisModdedChunkGenerator extends ChunkGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.biomeSource),
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
).apply(instance, IrisModdedChunkGenerator::new));
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static volatile ExecutorService genPool = createGenPool();
public static void startGenPool() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
}
public static void shutdownGenPool() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
"com.ishland.c2me.base.ModProperties",
"com.ishland.c2me.base.common.config.C2MEConfig",
"com.ishland.c2me.opts.chunkio.ModProperties",
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
};
for (String marker : markers) {
try {
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
return true;
} catch (Throwable ignored) {
}
}
return false;
}
private static ExecutorService createGenPool() {
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
threads, threads, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
thread.setDaemon(true);
return thread;
});
pool.allowCoreThreadTimeOut(true);
return pool;
}
private final String dimensionKey;
private final String defaultPack;
private final String defaultDimensionKey;
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile Engine engine;
private volatile String activePack;
private volatile String activeDimensionKey;
private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
super(biomeSource);
this.dimensionKey = dimensionKey;
int colon = dimensionKey.indexOf(':');
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
this.activePack = defaultPack;
this.activeDimensionKey = defaultDimensionKey;
}
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
this.missingBiomeWarnings.clear();
resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
this.missingBiomeWarnings.clear();
resetVanillaSpawnBiomes();
}
public synchronized void resetToDefault() {
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
}
public String activePack() {
return activePack;
}
public String activeDimensionKey() {
return activeDimensionKey;
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return CODEC;
}
private ServerLevel boundLevel() {
MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server == null) {
return null;
}
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() == this) {
return level;
}
}
return null;
}
private Engine engine() {
Engine cached = engine;
if (cached != null) {
return cached;
}
synchronized (this) {
if (engine != null) {
return engine;
}
ServerLevel level = boundLevel();
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
}
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
engine = created;
return created;
}
}
private Engine engineOrNull() {
Engine cached = engine;
if (cached != null) {
return cached;
}
try {
return engine();
} catch (Throwable ignored) {
return null;
}
}
public String dimensionKey() {
return dimensionKey;
}
public Engine engineIfBound() {
return engine;
}
public Engine commandEngine() {
return engine();
}
public long lastChunkGenAt() {
return lastChunkGenAt;
}
public void onHotload() {
biomeHolders.clear();
missingBiomeWarnings.clear();
resetVanillaSpawnBiomes();
}
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
biome, structureManager, category, pos);
if (resolvedSpawns != explicitSpawns) {
return resolvedSpawns;
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
if (explicitSpawns.isEmpty()) {
return vanillaSpawns;
}
if (vanillaSpawns.isEmpty()) {
return explicitSpawns;
}
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
if (vanillaSpawnBiomesInitialized) {
return;
}
Engine current = engineOrNull();
if (current == null) {
return;
}
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
}
private synchronized void resetVanillaSpawnBiomes() {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
Engine generationEngine = engine();
ChunkPos pos = chunk.getPos();
lastChunkGenAt = System.currentTimeMillis();
if (announced.compareAndSet(false, true)) {
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
}
LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z());
int dimMinY = generationEngine.getMinHeight();
int dimMaxY = generationEngine.getMaxHeight();
int height = dimMaxY - dimMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
Registry<Biome> biomeRegistry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
if (PARALLEL_CHUNK_SYSTEM) {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState));
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState),
genPool);
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
int dimMinY, int height, PlatformBlockState air,
Registry<Biome> biomeRegistry, RandomState randomState) {
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
} catch (GenerationSessionException e) {
if (e.isExpectedTeardown()) {
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
return chunk;
}
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
} catch (Throwable e) {
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
}
writeBlocks(chunk, blocks, dimMinY, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
return chunk;
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry) {
Holder<Biome> existing = biomeHolders.get("minecraft:plains");
if (existing != null) {
return existing;
}
Holder<Biome> resolved = registry.get(Identifier.fromNamespaceAndPath("minecraft", "plains"))
.<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
.orElseThrow(() -> new IllegalStateException("minecraft:plains missing from biome registry"));
Holder<Biome> raced = biomeHolders.putIfAbsent("minecraft:plains", resolved);
return raced != null ? raced : resolved;
}
private Holder<Biome> holderFor(Registry<Biome> registry, PlatformBiome biome) {
if (biome == null) {
return fallbackBiome(registry);
}
String key = biome.key();
Holder<Biome> existing = biomeHolders.get(key);
if (existing != null) {
return existing;
}
Identifier identifier = Identifier.tryParse(key);
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
}
return fallbackBiome(registry);
});
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
return raced != null ? raced : resolved;
}
public BiomeResolver regenBiomeResolver(Registry<Biome> registry, Hunk<PlatformBiome> biomes, ChunkPos pos) {
Engine current = engine();
int dimMinY = current.getMinHeight();
int height = current.getMaxHeight() - dimMinY;
return new HunkBiomeResolver(this, biomes, registry, pos, dimMinY, height);
}
private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) {
int chunkMinY = chunk.getMinY();
int chunkMaxY = chunkMinY + chunk.getHeight();
int from = Math.max(dimMinY, chunkMinY);
int to = Math.min(dimMinY + height, chunkMaxY);
for (int y = from; y < to; ) {
int sectionIndex = chunk.getSectionIndex(y);
LevelChunkSection section = chunk.getSection(sectionIndex);
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
int sectionEnd = Math.min(sectionMinY + 16, to);
section.acquire();
try {
for (int blockY = y; blockY < sectionEnd; blockY++) {
int bufferY = blockY - dimMinY;
int localY = blockY & 15;
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
if (blocks.isAir(x, bufferY, z)) {
continue;
}
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
section.setBlockState(x, localY, z, (BlockState) state.nativeHandle(), false);
}
}
}
} finally {
section.release();
}
y = sectionEnd;
}
}
private static final class HunkBiomeResolver implements BiomeResolver {
private final IrisModdedChunkGenerator generator;
private final Hunk<PlatformBiome> biomes;
private final Registry<Biome> registry;
private final ChunkPos pos;
private final int dimMinY;
private final int height;
private HunkBiomeResolver(IrisModdedChunkGenerator generator, Hunk<PlatformBiome> biomes, Registry<Biome> registry, ChunkPos pos, int dimMinY, int height) {
this.generator = generator;
this.biomes = biomes;
this.registry = registry;
this.pos = pos;
this.dimMinY = dimMinY;
this.height = height;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
int localX = QuartPos.toBlock(quartX) - pos.getMinBlockX();
int localZ = QuartPos.toBlock(quartZ) - pos.getMinBlockZ();
int bufferY = Math.max(0, Math.min(height - 1, QuartPos.toBlock(quartY) - dimMinY));
PlatformBiome biome = biomes.get(localX, bufferY, localZ);
return generator.holderFor(registry, biome);
}
}
@Override
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
}
@Override
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
}
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
}
@Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
}
@Override
public int getGenDepth() {
Engine current = engineOrNull();
return current == null ? 384 : current.getMaxHeight() - current.getMinHeight();
}
@Override
public int getSeaLevel() {
Engine current = engineOrNull();
return current == null ? 63 : current.getMinHeight() + current.getDimension().getFluidHeight();
}
@Override
public int getMinY() {
Engine current = engineOrNull();
return current == null ? -64 : current.getMinHeight();
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = engineOrNull();
if (current == null) {
return heightAccessor.getMinY() + Math.min(heightAccessor.getHeight(), 64);
}
boolean ignoreFluid = type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG;
return current.getMinHeight() + current.getHeight(x, z, ignoreFluid) + 1;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = engineOrNull();
BlockState airState = Blocks.AIR.defaultBlockState();
if (current == null) {
Arrays.fill(states, airState);
return new NoiseColumn(minY, states);
}
int surface = current.getMinHeight() + current.getHeight(x, z, true);
int fluid = current.getMinHeight() + current.getDimension().getFluidHeight();
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface - minY + 1));
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid - minY + 1)));
Arrays.fill(states, 0, solidEnd, stone);
Arrays.fill(states, solidEnd, fluidEnd, water);
Arrays.fill(states, fluidEnd, states.length, airState);
return new NoiseColumn(minY, states);
}
@Override
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
info.add("Iris dimension: " + dimensionKey);
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
}
@@ -0,0 +1,199 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public final class MainWorldService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String PRESET_NAMESPACE = "irisworldgen";
private static final String MARKER_NAME = "mainworld.pending";
private static final String[] VANILLA_DIMENSION_FOLDERS = {
"region",
"entities",
"poi",
"mantle",
"dimensions/minecraft/overworld",
"dimensions/minecraft/the_nether",
"dimensions/minecraft/the_end",
"DIM-1",
"DIM1"
};
private MainWorldService() {
}
public static String presetIdFor(String packRef) {
String value = packRef.trim();
int colon = value.indexOf(':');
String pack = colon >= 0 ? value.substring(0, colon) : value;
String dimension = colon >= 0 ? value.substring(colon + 1) : value;
String presetKey = dimension.equals(pack) ? pack : pack + "_" + dimension;
return PRESET_NAMESPACE + ":" + presetKey;
}
public static void reconcileEarly() {
try {
ModdedModConfig config = ModdedModConfig.get();
String pack = config.mainWorldPack();
if (pack == null || pack.isBlank()) {
return;
}
Path properties = instanceRoot().resolve("server.properties");
String target = presetIdFor(pack);
String currentType = readProperty(properties, "level-type");
if (!target.equals(currentType)) {
writeLevelProperties(properties, target, config.mainWorldSeed());
markPending();
LOGGER.warn("Iris main world '{}' staged: server.properties level-type set to {}. Restart again to generate it (this boot still uses the previous overworld; player data is kept).", pack, target);
return;
}
if (!isPending()) {
return;
}
String levelName = firstNonBlank(readProperty(properties, "level-name"), "world");
wipeVanillaDimensions(instanceRoot().resolve(levelName));
clearPending();
LOGGER.warn("Iris main world '{}' generated fresh: cleared the previous overworld/nether/end so this boot regenerates them as {} (player data kept).", pack, target);
} catch (Throwable e) {
LOGGER.error("Iris main world reconciliation failed", e);
}
}
public static boolean stage(String packRef, long seed) {
try {
Path properties = instanceRoot().resolve("server.properties");
writeLevelProperties(properties, presetIdFor(packRef), seed);
markPending();
return true;
} catch (IOException e) {
LOGGER.error("Iris failed to stage the main world in server.properties", e);
return false;
}
}
public static void clearOverride() {
try {
clearPending();
} catch (IOException e) {
LOGGER.error("Iris failed to clear the pending main world marker", e);
}
}
private static Path instanceRoot() {
return ModdedEngineBootstrap.loader().configDir().getParent();
}
private static Path markerFile() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve(MARKER_NAME);
}
private static boolean isPending() {
return Files.isRegularFile(markerFile());
}
private static void markPending() throws IOException {
Path marker = markerFile();
Files.createDirectories(marker.getParent());
Files.writeString(marker, "pending", StandardCharsets.UTF_8);
}
private static void clearPending() throws IOException {
Files.deleteIfExists(markerFile());
}
private static String readProperty(Path properties, String key) throws IOException {
if (!Files.isRegularFile(properties)) {
return null;
}
List<String> lines = Files.readAllLines(properties, StandardCharsets.UTF_8);
String prefix = key + "=";
for (String line : lines) {
if (line.startsWith(prefix)) {
return unescape(line.substring(prefix.length()).trim());
}
}
return null;
}
private static void writeLevelProperties(Path properties, String target, long seed) throws IOException {
List<String> lines = Files.isRegularFile(properties)
? new ArrayList<>(Files.readAllLines(properties, StandardCharsets.UTF_8))
: new ArrayList<>();
setProperty(lines, "level-type", escape(target));
if (seed != 0L) {
setProperty(lines, "level-seed", Long.toString(seed));
}
Files.write(properties, lines, StandardCharsets.UTF_8);
}
private static void setProperty(List<String> lines, String key, String value) {
String prefix = key + "=";
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith(prefix)) {
lines.set(i, prefix + value);
return;
}
}
lines.add(prefix + value);
}
private static void wipeVanillaDimensions(Path worldRoot) throws IOException {
Files.deleteIfExists(worldRoot.resolve("level.dat"));
Files.deleteIfExists(worldRoot.resolve("level.dat_old"));
for (String folder : VANILLA_DIMENSION_FOLDERS) {
deleteRecursively(worldRoot.resolve(folder));
}
}
private static void deleteRecursively(Path path) throws IOException {
if (!Files.exists(path)) {
return;
}
List<Path> entries = new ArrayList<>();
try (Stream<Path> walk = Files.walk(path)) {
walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
}
for (Path entry : entries) {
Files.deleteIfExists(entry);
}
}
private static String escape(String value) {
return value.replace(":", "\\:");
}
private static String unescape(String value) {
return value.replace("\\:", ":").replace("\\=", "=");
}
private static String firstNonBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformBiome;
import net.minecraft.world.level.biome.Biome;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedBiome implements PlatformBiome {
private static final ConcurrentHashMap<String, ModdedBiome> CACHE = new ConcurrentHashMap<>();
private final Biome biome;
private final String key;
private final String namespace;
private ModdedBiome(Biome biome, String key) {
this.biome = biome;
this.key = key;
int colon = key.indexOf(':');
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
}
public static ModdedBiome of(Biome biome, String key) {
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBiome(biome, k));
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public Object nativeHandle() {
return biome;
}
}
@@ -0,0 +1,142 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public final class ModdedBiomeWriter implements PlatformBiomeWriter {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String VANILLA_FALLBACK_KEY = "minecraft:plains";
private final Supplier<MinecraftServer> server;
public ModdedBiomeWriter(Supplier<MinecraftServer> server) {
this.server = server;
}
@Override
public int biomeIdFor(String key) {
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return 0;
}
int direct = idForKey(registry, key);
if (direct >= 0) {
return direct;
}
int derivative = idForDerivative(registry, key);
if (derivative >= 0) {
return derivative;
}
return fallbackId(registry);
}
@Override
public List<PlatformBiome> allBiomes() {
Registry<Biome> registry = biomeRegistry();
List<PlatformBiome> biomes = new ArrayList<>();
if (registry == null) {
return biomes;
}
for (Identifier identifier : registry.keySet()) {
Biome biome = registry.getValue(identifier);
if (biome != null) {
biomes.add(ModdedBiome.of(biome, identifier.toString()));
}
}
return biomes;
}
private int idForKey(Registry<Biome> registry, String key) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return -1;
}
Biome biome = registry.getValue(identifier);
return biome == null ? -1 : registry.getId(biome);
}
private int idForDerivative(Registry<Biome> registry, String key) {
int colon = key.indexOf(':');
if (colon <= 0 || colon >= key.length() - 1) {
return -1;
}
String dimensionLoadKey = key.substring(0, colon);
String customBiomeId = key.substring(colon + 1);
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
if (owner == null) {
return -1;
}
String derivativeKey = owner.getVanillaDerivativeKey();
if (derivativeKey == null) {
return -1;
}
return idForKey(registry, derivativeKey);
}
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) {
continue;
}
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
continue;
}
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (customBiomeId.equals(custom.getId())) {
return biome;
}
}
}
}
return null;
}
private int fallbackId(Registry<Biome> registry) {
int plains = idForKey(registry, VANILLA_FALLBACK_KEY);
return plains >= 0 ? plains : 0;
}
private Registry<Biome> biomeRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
return null;
}
return instance.registryAccess().lookupOrThrow(Registries.BIOME);
}
}
@@ -0,0 +1,90 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
public final class ModdedBlockBuffer implements Hunk<PlatformBlockState> {
private final PlatformBlockState[] data;
private final PlatformBlockState air;
private final int height;
public ModdedBlockBuffer(int height, PlatformBlockState air) {
this.data = new PlatformBlockState[16 * height * 16];
this.air = air;
this.height = height;
}
private int index(int x, int y, int z) {
return (y * 16 + z) * 16 + x;
}
public boolean isAir(int x, int y, int z) {
return data[index(x, y, z)] == null;
}
@Override
public int getWidth() {
return 16;
}
@Override
public int getDepth() {
return 16;
}
@Override
public int getHeight() {
return height;
}
@Override
public void set(int x, int y, int z, PlatformBlockState t) {
if (t == null) {
return;
}
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
return;
}
data[index(x, y, z)] = t;
}
@Override
public void setRaw(int x, int y, int z, PlatformBlockState t) {
if (t == null) {
return;
}
data[index(x, y, z)] = t;
}
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
PlatformBlockState state = data[index(x, y, z)];
return state == null ? air : state;
}
@Override
public PlatformBlockState get(int x, int y, int z) {
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
return air;
}
return getRaw(x, y, z);
}
}
@@ -0,0 +1,504 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LeavesBlock;
import net.minecraft.world.level.block.PointedDripstoneBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public final class ModdedBlockResolution {
private static final Set<Block> FOLIAGE = blockSet(
"poppy", "dandelion", "cornflower", "sweet_berry_bush", "crimson_roots", "warped_roots",
"nether_sprouts", "allium", "azure_bluet", "blue_orchid", "oxeye_daisy", "lily_of_the_valley",
"wither_rose", "dark_oak_sapling", "acacia_sapling", "jungle_sapling", "birch_sapling",
"spruce_sapling", "oak_sapling", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
"fern", "large_fern", "short_grass", "tall_grass");
private static final Set<Block> DECORANT = decorantSet();
private static final Set<Block> LIT = blockSet(
"glowstone", "amethyst_cluster", "small_amethyst_bud", "medium_amethyst_bud", "large_amethyst_bud",
"end_rod", "soul_sand", "torch", "redstone_torch", "soul_torch", "redstone_wall_torch", "wall_torch",
"soul_wall_torch", "lantern", "candle", "jack_o_lantern", "redstone_lamp", "magma_block", "light",
"shroomlight", "sea_lantern", "soul_lantern", "fire", "soul_fire", "sea_pickle", "brewing_stand",
"redstone_ore");
private static final Set<Block> STORAGE = blockSet(
"chest", "smoker", "trapped_chest", "shulker_box", "white_shulker_box", "orange_shulker_box",
"magenta_shulker_box", "light_blue_shulker_box", "yellow_shulker_box", "lime_shulker_box",
"pink_shulker_box", "gray_shulker_box", "light_gray_shulker_box", "cyan_shulker_box",
"purple_shulker_box", "blue_shulker_box", "brown_shulker_box", "green_shulker_box",
"red_shulker_box", "black_shulker_box", "barrel", "dispenser", "dropper", "hopper", "furnace",
"blast_furnace");
private static final Set<Block> STORAGE_CHEST = storageChestSet();
private static final Set<Block> DEEPSLATE = blockSet(
"deepslate", "deepslate_bricks", "deepslate_brick_slab", "deepslate_brick_stairs",
"deepslate_brick_wall", "deepslate_tile_slab", "deepslate_tiles", "deepslate_tile_stairs",
"deepslate_tile_wall", "cracked_deepslate_tiles", "deepslate_coal_ore", "deepslate_iron_ore",
"deepslate_copper_ore", "deepslate_diamond_ore", "deepslate_emerald_ore", "deepslate_gold_ore",
"deepslate_lapis_ore", "deepslate_redstone_ore");
private static final Map<Block, Block> NORMAL_TO_DEEPSLATE = oreMap(false);
private static final Map<Block, Block> DEEPSLATE_TO_NORMAL = oreMap(true);
private static final Set<Block> FOLIAGE_PLANTABLE_STATE = blockSet(
"grass_block", "moss_block", "rooted_dirt", "dirt", "coarse_dirt", "podzol");
private static final Set<Block> FOLIAGE_PLANTABLE_MATERIAL = blockSet(
"grass_block", "moss_block", "dirt", "tall_grass", "tall_seagrass", "large_fern", "sunflower",
"peony", "lilac", "rose_bush", "rooted_dirt", "coarse_dirt", "podzol");
private static final Set<Block> PLACE_ONTO_LEAVES = blockSet(
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
private ModdedBlockResolution() {
}
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties) {
}
private static Set<Block> blockSet(String... ids) {
Set<Block> blocks = new HashSet<>();
for (String id : ids) {
Identifier identifier = Identifier.parse("minecraft:" + id);
if (BuiltInRegistries.BLOCK.containsKey(identifier)) {
blocks.add(BuiltInRegistries.BLOCK.getValue(identifier));
}
}
return blocks;
}
private static Set<Block> decorantSet() {
Set<Block> blocks = blockSet(
"short_grass", "tall_grass", "fern", "large_fern", "cornflower", "sunflower", "chorus_flower",
"poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
"lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid",
"lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom",
"crimson_roots", "azure_bluet", "weeping_vines", "weeping_vines_plant", "warped_roots",
"nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes",
"carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass",
"acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button",
"oak_button", "polished_blackstone_button", "spruce_button", "stone_button", "warped_button",
"torch", "soul_torch", "glow_lichen", "vine", "sculk_vein");
blocks.addAll(FOLIAGE);
return blocks;
}
private static Set<Block> storageChestSet() {
Set<Block> blocks = new HashSet<>(STORAGE);
blocks.remove(Blocks.SMOKER);
blocks.remove(Blocks.FURNACE);
blocks.remove(Blocks.BLAST_FURNACE);
return blocks;
}
private static Map<Block, Block> oreMap(boolean deepslateToNormal) {
String[][] pairs = {
{"coal_ore", "deepslate_coal_ore"},
{"emerald_ore", "deepslate_emerald_ore"},
{"diamond_ore", "deepslate_diamond_ore"},
{"copper_ore", "deepslate_copper_ore"},
{"gold_ore", "deepslate_gold_ore"},
{"iron_ore", "deepslate_iron_ore"},
{"lapis_ore", "deepslate_lapis_ore"},
{"redstone_ore", "deepslate_redstone_ore"}
};
Map<Block, Block> map = new HashMap<>();
for (String[] pair : pairs) {
Block normal = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[0]));
Block deep = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[1]));
if (deepslateToNormal) {
map.put(deep, normal);
} else {
map.put(normal, deep);
}
}
return map;
}
private static void warnUnresolved(String key, String message) {
if (UNRESOLVED.firstOccurrence(key)) {
IrisLogging.warn(message);
}
String summary = UNRESOLVED.pollSummary();
if (summary != null) {
IrisLogging.warn(summary);
}
}
public static BlockState airState() {
return AIR;
}
public static ModdedBlockState getAir() {
return ModdedBlockState.of(AIR, null);
}
public static ModdedBlockState get(String bdxf) {
Parsed parsed = resolveGet(bdxf);
return ModdedBlockState.of(parsed.state(), parsed.properties());
}
public static ModdedBlockState getNoCompat(String bdxf) {
Parsed parsed = resolveNoCompat(bdxf);
return ModdedBlockState.of(parsed.state(), parsed.properties());
}
public static ModdedBlockState getOrNull(String bdxf) {
return getOrNull(bdxf, false);
}
public static ModdedBlockState getOrNull(String bdxf, boolean warn) {
Parsed parsed = resolveOrNull(bdxf, warn);
return parsed == null ? null : ModdedBlockState.of(parsed.state(), parsed.properties());
}
static Parsed resolveGet(String bdxf) {
Parsed parsed = resolveOrNull(bdxf, false);
if (parsed != null) {
return parsed;
}
IrisLogging.error("Can't find block data for " + bdxf);
return new Parsed(AIR, null);
}
static Parsed resolveNoCompat(String bdxf) {
Parsed parsed = resolveOrNull(bdxf, true);
if (parsed != null) {
return parsed;
}
return new Parsed(AIR, null);
}
static Parsed resolveOrNull(String bdxf, boolean warn) {
try {
String bd = bdxf.trim();
if (bd.startsWith("minecraft:cauldron[level=")) {
bd = bd.replaceAll("\\Q:cauldron[\\E", ":water_cauldron[");
}
if (bd.equals("minecraft:grass_path")) {
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null);
}
Parsed bdx = parseBlockData(bd, warn);
if (bdx == null) {
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd);
if (provided != null) {
return new Parsed(provided, null);
}
if (warn) {
warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
}
return new Parsed(AIR, null);
}
return bdx;
} catch (Throwable e) {
e.printStackTrace();
if (warn) {
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
}
}
return null;
}
public static ModdedBlockState strictParse(String key) {
Parsed parsed = parseStrict(key);
return ModdedBlockState.of(parsed.state(), parsed.properties());
}
private static Parsed parseStrict(String key) {
StringReader reader = new StringReader(key);
BlockStateParser.BlockResult result;
try {
result = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false);
} catch (CommandSyntaxException e) {
throw new IllegalArgumentException("Could not parse data: " + key, e);
}
if (reader.canRead()) {
throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining());
}
return new Parsed(result.blockState(), result.properties());
}
private static Parsed createBlockData(String s, boolean warn) {
try {
return parseStrict(s);
} catch (IllegalArgumentException e) {
if (s.contains("[")) {
return createBlockData(s.split("\\Q[\\E")[0], warn);
}
}
if (warn) {
IrisLogging.warn("Can't find block data for " + s);
}
return null;
}
private static Parsed materialBlockData(String ix) {
if (ix.contains("[") || ix.contains(":")) {
return null;
}
Identifier identifier = Identifier.tryParse("minecraft:" + ix.toLowerCase(Locale.ROOT));
if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) {
return null;
}
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null);
}
private static Parsed parseBlockData(String ix, boolean warn) {
try {
Parsed bx = createBlockData(ix.toLowerCase(), warn);
if (bx == null) {
bx = createBlockData("minecraft:" + ix.toLowerCase(), warn);
}
if (bx == null) {
bx = materialBlockData(ix);
}
if (bx == null) {
if (warn) {
warnUnresolved(ix, "Unknown Block Data: " + ix);
}
return null;
}
if (bx.state().getBlock() instanceof LeavesBlock) {
BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay());
bx = new Parsed(mutated, bx.properties());
}
return bx;
} catch (Throwable e) {
String block = ix.contains(":") ? ix.split(":")[1].toLowerCase() : ix.toLowerCase();
String state = block.contains("[") ? block.split("\\Q[\\E")[1].split("\\Q]\\E")[0] : "";
Map<String, String> stateMap = new HashMap<>();
if (!state.equals("")) {
Arrays.stream(state.split(",")).forEach((String s) -> stateMap.put(s.split("=")[0], s.split("=")[1]));
}
block = block.split("\\Q[\\E")[0];
switch (block) {
case "cauldron" -> block = "water_cauldron";
case "grass_path" -> block = "dirt_path";
case "concrete" -> block = "white_concrete";
case "wool" -> block = "white_wool";
case "beetroots" -> {
if (stateMap.containsKey("age")) {
String updated = stateMap.get("age");
switch (updated) {
case "7" -> updated = "3";
case "3", "4", "5" -> updated = "2";
case "1", "2" -> updated = "1";
}
stateMap.put("age", updated);
}
}
}
Map<String, String> newStates = new HashMap<>();
for (String key : stateMap.keySet()) {
createBlockData(block + "[" + key + "=" + stateMap.get(key) + "]", false);
newStates.put(key, stateMap.get(key));
}
String joined = newStates.entrySet().stream()
.map((Map.Entry<String, String> entry) -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining(","));
if (!joined.equals("")) {
joined = "[" + joined + "]";
}
String newBlock = block + joined;
IrisLogging.debug("Converting " + ix + " to " + newBlock);
try {
return createBlockData(newBlock, false);
} catch (Throwable e1) {
IrisLogging.reportError(e1);
}
return null;
}
}
private static boolean shouldPreventLeafDecay() {
return IrisSettings.get().getGenerator().isPreventLeafDecay();
}
public static BlockState toDeepSlateOre(BlockState block, BlockState ore) {
Block key = ore.getBlock();
if (isDeepSlate(block)) {
Block mapped = NORMAL_TO_DEEPSLATE.get(key);
if (mapped != null) {
return mapped.defaultBlockState();
}
} else {
Block mapped = DEEPSLATE_TO_NORMAL.get(key);
if (mapped != null) {
return mapped.defaultBlockState();
}
}
return ore;
}
public static boolean isDeepSlate(BlockState state) {
return DEEPSLATE.contains(state.getBlock());
}
public static boolean isOre(BlockState state) {
return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore");
}
public static boolean isAir(BlockState state) {
if (state == null) {
return true;
}
Block block = state.getBlock();
return block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR;
}
public static boolean isSolid(BlockState state) {
if (state == null) {
return false;
}
return isSolidMaterial(state.getBlock());
}
private static boolean isSolidMaterial(Block block) {
return block.defaultBlockState().blocksMotion();
}
public static boolean isOccluding(BlockState state) {
return state.getBlock().defaultBlockState().isRedstoneConductor(EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
}
public static boolean isFluid(BlockState state) {
Block block = state.getBlock();
return block == Blocks.WATER || block == Blocks.LAVA;
}
public static boolean isWater(BlockState state) {
return state.getBlock() == Blocks.WATER;
}
public static boolean isWaterLogged(BlockState state) {
return state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED);
}
public static boolean isLit(BlockState state) {
return LIT.contains(state.getBlock());
}
public static boolean isUpdatable(BlockState state) {
return isStorage(state)
|| (state.getBlock() instanceof PointedDripstoneBlock
&& state.getValue(PointedDripstoneBlock.THICKNESS) == SpeleothemThickness.TIP);
}
public static boolean isFoliage(BlockState state) {
return FOLIAGE.contains(state.getBlock());
}
public static boolean isFoliagePlantable(BlockState state) {
return FOLIAGE_PLANTABLE_STATE.contains(state.getBlock());
}
public static boolean isDecorant(BlockState state) {
return DECORANT.contains(state.getBlock());
}
public static boolean isStorage(BlockState state) {
return STORAGE.contains(state.getBlock());
}
public static boolean isStorageChest(BlockState state) {
return STORAGE_CHEST.contains(state.getBlock());
}
public static boolean isVineBlock(BlockState state) {
Block block = state.getBlock();
return block == Blocks.VINE || block == Blocks.SCULK_VEIN || block == Blocks.GLOW_LICHEN;
}
public static boolean hasTileEntity(BlockState state) {
return state.getBlock().defaultBlockState().hasBlockEntity();
}
public static boolean canPlaceOnto(Block mat, Block onto) {
if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM)
&& (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS
|| mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) {
return true;
}
if (FOLIAGE.contains(mat)) {
if (!FOLIAGE_PLANTABLE_MATERIAL.contains(onto)) {
return false;
}
}
if (onto == Blocks.AIR || onto == Blocks.CAVE_AIR || onto == Blocks.VOID_AIR) {
return false;
}
if (onto == Blocks.GRASS_BLOCK && mat == Blocks.DEAD_BUSH) {
return false;
}
if (onto == Blocks.DIRT_PATH) {
if (!isSolidMaterial(mat)) {
return false;
}
}
if (PLACE_ONTO_LEAVES.contains(onto)) {
return isSolidMaterial(mat);
}
return true;
}
}
@@ -0,0 +1,308 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class ModdedBlockState implements PlatformBlockState {
private static final ConcurrentHashMap<String, ModdedBlockState> CACHE = new ConcurrentHashMap<>();
private final BlockState state;
private final Map<Property<?>, Comparable<?>> parsedProperties;
private final String key;
private final String namespace;
private volatile Boolean air;
private volatile Boolean solid;
private volatile Boolean occluding;
private volatile Boolean fluid;
private volatile Boolean water;
private volatile Boolean foliage;
private volatile Boolean decorant;
private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key) {
this.state = state;
this.parsedProperties = parsedProperties;
this.key = key;
this.namespace = parseNamespace(key);
}
public static ModdedBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) {
String key = serialize(state);
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k));
}
public static String serialize(BlockState state) {
StringBuilder result = new StringBuilder(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
if (!state.isSingletonState()) {
result.append('[');
result.append(state.getValues()
.map((Property.Value<?> value) -> value.property().getName() + "=" + value.valueName())
.collect(Collectors.joining(",")));
result.append(']');
}
return result.toString();
}
public BlockState handle() {
return state;
}
Map<Property<?>, Comparable<?>> parsedProperties() {
return parsedProperties;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ModdedBlockState blockState)) {
return false;
}
return state.equals(blockState.state);
}
@Override
public int hashCode() {
return key.hashCode();
}
private static String parseNamespace(String key) {
String base = key;
int bracket = base.indexOf('[');
if (bracket >= 0) {
base = base.substring(0, bracket);
}
int colon = base.indexOf(':');
return colon >= 0 ? base.substring(0, colon) : "minecraft";
}
private static String mergeProperty(String key, String name, String value) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return key + "[" + name + "=" + value + "]";
}
String base = key.substring(0, bracket);
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
properties.put(name, value);
StringBuilder merged = new StringBuilder(base).append('[');
boolean first = true;
for (Map.Entry<String, String> property : properties.entrySet()) {
if (!first) {
merged.append(',');
}
merged.append(property.getKey()).append('=').append(property.getValue());
first = false;
}
return merged.append(']').toString();
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return namespace;
}
@Override
public boolean isAir() {
Boolean cached = air;
if (cached == null) {
cached = ModdedBlockResolution.isAir(state);
air = cached;
}
return cached;
}
@Override
public boolean isSolid() {
Boolean cached = solid;
if (cached == null) {
cached = ModdedBlockResolution.isSolid(state);
solid = cached;
}
return cached;
}
@Override
public boolean isOccluding() {
Boolean cached = occluding;
if (cached == null) {
cached = ModdedBlockResolution.isOccluding(state);
occluding = cached;
}
return cached;
}
@Override
public boolean isCustom() {
return false;
}
@Override
public boolean isFluid() {
Boolean cached = fluid;
if (cached == null) {
cached = ModdedBlockResolution.isFluid(state);
fluid = cached;
}
return cached;
}
@Override
public boolean isWater() {
Boolean cached = water;
if (cached == null) {
cached = ModdedBlockResolution.isWater(state);
water = cached;
}
return cached;
}
@Override
public boolean isWaterLogged() {
return ModdedBlockResolution.isWaterLogged(state);
}
@Override
public boolean isLit() {
return ModdedBlockResolution.isLit(state);
}
@Override
public boolean isUpdatable() {
return ModdedBlockResolution.isUpdatable(state);
}
@Override
public boolean isFoliage() {
Boolean cached = foliage;
if (cached == null) {
cached = ModdedBlockResolution.isFoliage(state);
foliage = cached;
}
return cached;
}
@Override
public boolean isFoliagePlantable() {
return ModdedBlockResolution.isFoliagePlantable(state);
}
@Override
public boolean isDecorant() {
Boolean cached = decorant;
if (cached == null) {
cached = ModdedBlockResolution.isDecorant(state);
decorant = cached;
}
return cached;
}
@Override
public boolean isStorage() {
return ModdedBlockResolution.isStorage(state);
}
@Override
public boolean isStorageChest() {
return ModdedBlockResolution.isStorageChest(state);
}
@Override
public boolean isOre() {
return ModdedBlockResolution.isOre(state);
}
@Override
public boolean isDeepSlate() {
return ModdedBlockResolution.isDeepSlate(state);
}
@Override
public boolean isVineBlock() {
return ModdedBlockResolution.isVineBlock(state);
}
@Override
public boolean canPlaceOnto(PlatformBlockState onto) {
return ModdedBlockResolution.canPlaceOnto(state.getBlock(), ((BlockState) onto.nativeHandle()).getBlock());
}
@Override
public boolean matches(PlatformBlockState other) {
if (!(other instanceof ModdedBlockState blockState)) {
return false;
}
if (state.getBlock() != blockState.state.getBlock()) {
return false;
}
if (state.equals(blockState.state)) {
return true;
}
if (blockState.parsedProperties == null) {
return false;
}
BlockState merged = state;
for (Map.Entry<Property<?>, Comparable<?>> entry : blockState.parsedProperties.entrySet()) {
merged = applyProperty(merged, entry.getKey(), entry.getValue());
}
return merged.equals(state);
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> BlockState applyProperty(BlockState state, Property<?> property, Comparable<?> value) {
return state.setValue((Property<T>) property, (T) value);
}
@Override
public boolean hasTileEntity() {
return ModdedBlockResolution.hasTileEntity(state);
}
@Override
public PlatformBlockState withProperty(String name, String value) {
String merged = mergeProperty(key, name, value);
return ModdedBlockResolution.strictParse(merged);
}
@Override
public Object nativeHandle() {
return state;
}
}
@@ -0,0 +1,130 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.ChunkGenerator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedDeathLoot {
private static final String TAG_PREFIX = "iris_loot|";
private static final int LOOT_RNG_SALT = 345911;
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
private ModdedDeathLoot() {
}
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
if (entity == null || tableKeys == null || tableKeys.isEmpty()) {
return;
}
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys));
}
public static void handle(LivingEntity entity) {
if (entity == null || !(entity.level() instanceof ServerLevel level)) {
return;
}
String encoded = findTag(entity);
if (encoded == null) {
return;
}
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
if (parts.length < 5) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return;
}
long seed;
int spawnX;
int spawnY;
int spawnZ;
try {
seed = Long.parseLong(parts[0]);
spawnX = Integer.parseInt(parts[1]);
spawnY = Integer.parseInt(parts[2]);
spawnZ = Integer.parseInt(parts[3]);
} catch (NumberFormatException error) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return;
}
Engine engine = engineFor(level);
if (engine == null) {
return;
}
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT);
KList<ItemStack> drops = new KList<>();
for (String key : parts[4].split(",")) {
String trimmed = key.trim();
if (trimmed.isEmpty()) {
continue;
}
IrisLootTable table = engine.getData().getLootLoader().load(trimmed);
if (table == null) {
if (WARNED_TABLES.add(trimmed)) {
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'");
}
continue;
}
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ));
}
emit(level, entity, drops);
}
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
double x = entity.getX();
double y = entity.getY() + 0.5D;
double z = entity.getZ();
for (ItemStack stack : drops) {
if (stack == null || stack.isEmpty()) {
continue;
}
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack);
itemEntity.setDefaultPickUpDelay();
level.addFreshEntity(itemEntity);
}
}
private static String findTag(LivingEntity entity) {
for (String tag : entity.entityTags()) {
if (tag.startsWith(TAG_PREFIX)) {
return tag;
}
}
return null;
}
private static Engine engineFor(ServerLevel level) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (generator instanceof IrisModdedChunkGenerator irisGenerator) {
return irisGenerator.engineIfBound();
}
return null;
}
}
@@ -0,0 +1,117 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.SupportType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap;
import java.util.Map;
public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFixer, DecoratorPlatformHooks.SurfaceSturdiness {
private static final Direction[] CARTESIAN = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN};
private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"};
@Override
public PlatformBlockState fixFaces(PlatformBlockState state, Hunk<PlatformBlockState> hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) {
ModdedBlockState fabric = (ModdedBlockState) state;
BlockState cloned = fabric.handle();
Map<String, BooleanProperty> allowed = faceProperties(cloned);
for (Map.Entry<String, BooleanProperty> entry : allowed.entrySet()) {
if (cloned.getValue(entry.getValue())) {
cloned = cloned.setValue(entry.getValue(), Boolean.FALSE);
}
}
boolean found = false;
for (Direction f : CARTESIAN) {
int yy = y + f.getStepY();
PlatformBlockState rs = null;
if (mantle != null) {
rs = mantle.getMantle().get(x + f.getStepX(), yy, z + f.getStepZ(), PlatformBlockState.class);
}
BlockState r = rs == null ? (BlockState) EngineMantle.AIR.nativeHandle() : (BlockState) rs.nativeHandle();
if (isFaceSturdy(r, f.getOpposite())) {
BooleanProperty property = allowed.get(f.getSerializedName());
if (property != null) {
found = true;
cloned = cloned.setValue(property, Boolean.TRUE);
}
continue;
}
int xx = rX + f.getStepX();
int zz = rZ + f.getStepZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
continue;
}
r = (BlockState) hunk.get(xx, yy, zz).nativeHandle();
if (isFaceSturdy(r, f.getOpposite())) {
BooleanProperty property = allowed.get(f.getSerializedName());
if (property != null) {
found = true;
cloned = cloned.setValue(property, Boolean.TRUE);
}
}
}
if (!found) {
String fallback = allowed.containsKey("down") ? "down" : "up";
BooleanProperty property = allowed.get(fallback);
if (property != null) {
cloned = cloned.setValue(property, Boolean.TRUE);
}
}
return ModdedBlockState.of(cloned, fabric.parsedProperties());
}
@Override
public boolean canGoOn(PlatformBlockState surface) {
return isFaceSturdy((BlockState) surface.nativeHandle(), Direction.UP);
}
private static boolean isFaceSturdy(BlockState state, Direction face) {
return state.isFaceSturdy(EmptyBlockGetter.INSTANCE, BlockPos.ZERO, face, SupportType.FULL);
}
private static Map<String, BooleanProperty> faceProperties(BlockState state) {
Map<String, BooleanProperty> properties = new LinkedHashMap<>();
for (String name : FACE_NAMES) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals(name) && property instanceof BooleanProperty bool) {
properties.put(name, bool);
}
}
}
return properties;
}
}
@@ -0,0 +1,331 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.core.BlockPos;
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.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.DerivedLevelData;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.ServerLevelData;
import net.minecraft.world.level.storage.WorldData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
public final class ModdedDimensionManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Object LOCK = new Object();
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
private static volatile ModdedServerAccess access;
private ModdedDimensionManager() {
}
public static void bindAccess(ModdedServerAccess serverAccess) {
access = serverAccess;
}
public static void clear() {
HANDLES.clear();
}
public static Handle handle(String dimensionId) {
return HANDLES.get(dimensionId);
}
public static List<Handle> handles() {
return new ArrayList<>(HANDLES.values());
}
public static ServerLevel level(MinecraftServer server, String dimensionId) {
Handle handle = HANDLES.get(dimensionId);
if (handle != null && handle.level().getServer() == server) {
return handle.level();
}
ResourceKey<Level> key = levelKey(dimensionId);
for (ServerLevel level : server.getAllLevels()) {
if (level.dimension().equals(key)) {
return level;
}
}
return null;
}
public static Engine engine(MinecraftServer server, String dimensionId) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return null;
}
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
return null;
}
return generator.commandEngine();
}
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) {
existing.generator().repoint(pack, packDimensionKey, seed);
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed);
return refreshed;
}
if (serverAccess.hasLevel(server, key)) {
ServerLevel present = level(server, dimensionId);
if (present == null || !(present.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
}
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
generator.repoint(pack, packDimensionKey, seed);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle);
return handle;
}
try {
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
HANDLES.put(dimensionId, handle);
LOGGER.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
return handle;
} catch (Throwable e) {
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
}
}
}
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
Handle handle = create(server, dimensionId, pack, packDimensionKey, seed);
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
return handle;
}
public static boolean removePersistent(MinecraftServer server, String dimensionId, boolean wipeStorage) {
boolean removed = remove(server, dimensionId, wipeStorage);
ModdedDimensionRegistryStore.remove(server, dimensionId);
return removed;
}
public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) {
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
ServerLevel level = level(server, dimensionId);
if (level == null) {
HANDLES.remove(dimensionId);
if (wipeStorage) {
ModdedDimensionStorage.wipe(server, key);
}
return false;
}
try {
evacuate(server, level);
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.unbindEngine();
}
ModdedWorldEngines.evict(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key);
level.close();
HANDLES.remove(dimensionId);
if (wipeStorage) {
ModdedDimensionStorage.wipe(server, key);
}
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
return true;
} catch (Throwable e) {
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
}
}
}
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return false;
}
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
completeTeleport(player, level, x, y, z, blockX, blockZ);
return true;
}
UUID playerId = player.getUUID();
CompletableFuture
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
.thenCompose((CompletableFuture<?> inner) -> inner)
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
if (error != null) {
LOGGER.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
}
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
if (target == null) {
return;
}
completeTeleport(target, level, x, y, z, blockX, blockZ);
}));
return true;
}
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
double targetY = y;
if (y == Double.MIN_VALUE) {
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
}
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
}
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
if (dimension != null) {
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
if (packType.isPresent()) {
return packType.get();
}
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
}
}
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
return registry.get(studioPool)
.map(reference -> (Holder<DimensionType>) reference)
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
}
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
try {
File packFolder = ModdedWorldEngines.packFolder(pack);
if (!packFolder.isDirectory()) {
return null;
}
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
} catch (Throwable e) {
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
return null;
}
}
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
RegistryAccess registryAccess = server.registryAccess();
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
generator.repoint(pack, packDimensionKey, seed);
LevelStem stem = new LevelStem(dimensionType, generator);
WorldData worldData = server.getWorldData();
ServerLevelData overworldData = worldData.overworldData();
DerivedLevelData derivedLevelData = new DerivedLevelData(worldData, overworldData);
Executor executor = serverAccess.levelExecutor(server);
LevelStorageSource.LevelStorageAccess storage = serverAccess.levelStorage(server);
long obfuscatedSeed = BiomeManager.obfuscateSeed(seed);
ServerLevel level = new ServerLevel(
server,
executor,
storage,
derivedLevelData,
key,
stem,
false,
obfuscatedSeed,
List.of(),
false);
serverAccess.putLevel(server, key, level);
server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
}
public static int evacuate(MinecraftServer server, ServerLevel from) {
ServerLevel fallback = server.overworld();
if (fallback == from) {
return 0;
}
BlockPos spawn = fallback.getRespawnData().pos();
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawn.getX(), spawn.getZ());
List<ServerPlayer> players = new ArrayList<>(from.players());
for (ServerPlayer player : players) {
player.teleportTo(fallback, spawn.getX() + 0.5D, spawnY, spawn.getZ() + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
}
return players.size();
}
private static ResourceKey<Level> levelKey(String dimensionId) {
Identifier identifier = Identifier.parse(dimensionId);
return ResourceKey.create(Registries.DIMENSION, identifier);
}
private static ModdedServerAccess requireAccess() {
ModdedServerAccess bound = access;
if (bound == null) {
throw new IllegalStateException("Iris modded server access is not bound; the loader bootstrap must bind ModdedServerAccess before runtime dimension injection");
}
return bound;
}
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
}
}
@@ -0,0 +1,132 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ModdedDimensionRegistryStore {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String FILE_NAME = "iris-dimensions.json";
private ModdedDimensionRegistryStore() {
}
public static List<PersistentDimension> load(MinecraftServer server) {
Path file = storeFile(server);
if (!Files.isRegularFile(file)) {
return new ArrayList<>();
}
try {
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
JSONArray entries = root.optJSONArray("dimensions");
if (entries == null) {
return new ArrayList<>();
}
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
for (int index = 0; index < entries.length(); index++) {
JSONObject entry = entries.getJSONObject(index);
String id = entry.optString("id", null);
if (id == null) {
continue;
}
String pack = entry.optString("pack", null);
String dimension = entry.optString("dimension", null);
if (pack == null || dimension == null) {
LOGGER.error("Iris registry entry '{}' in {} has no pack/dimension fields; skipping it. Re-create the world with /iris world enable", id, file);
continue;
}
if (!entry.has("seed")) {
LOGGER.warn("Iris registry entry '{}' in {} has no seed; skipping it. Re-create the world with /iris world enable", id, file);
continue;
}
deduplicated.put(id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
}
return new ArrayList<>(deduplicated.values());
} catch (RuntimeException | IOException e) {
LOGGER.error("Iris persistent dimension registry at {} is invalid; ignoring it", file, e);
return new ArrayList<>();
}
}
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
Map<String, PersistentDimension> current = index(load(server));
current.put(dimension.id(), dimension);
write(server, new ArrayList<>(current.values()));
}
public static synchronized void remove(MinecraftServer server, String id) {
Map<String, PersistentDimension> current = index(load(server));
if (current.remove(id) != null) {
write(server, new ArrayList<>(current.values()));
}
}
private static Map<String, PersistentDimension> index(List<PersistentDimension> dimensions) {
Map<String, PersistentDimension> map = new LinkedHashMap<>();
for (PersistentDimension dimension : dimensions) {
map.put(dimension.id(), dimension);
}
return map;
}
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
Path file = storeFile(server);
JSONArray entries = new JSONArray();
for (PersistentDimension dimension : dimensions) {
JSONObject entry = new JSONObject();
entry.put("id", dimension.id());
entry.put("pack", dimension.pack());
entry.put("dimension", dimension.dimension());
entry.put("seed", dimension.seed());
entries.put(entry);
}
JSONObject root = new JSONObject();
root.put("dimensions", entries);
try {
Files.createDirectories(file.getParent());
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
Files.writeString(temp, root.toString(2), StandardCharsets.UTF_8);
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
LOGGER.error("Iris failed to write persistent dimension registry at {}", file, e);
}
}
private static Path storeFile(MinecraftServer server) {
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
}
public record PersistentDimension(String id, String pack, String dimension, long seed) {
}
}
@@ -0,0 +1,68 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public final class ModdedDimensionStorage {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
private ModdedDimensionStorage() {
}
public static File storageFolder(MinecraftServer server, ResourceKey<Level> dimension) {
return DimensionType.getStorageFolder(dimension, server.getWorldPath(LevelResource.ROOT)).toFile();
}
public static void wipe(MinecraftServer server, ResourceKey<Level> dimension) {
File storageFolder = storageFolder(server, dimension);
for (String folder : CHUNK_DATA_FOLDERS) {
deleteRecursively(new File(storageFolder, folder).toPath());
}
LOGGER.info("Iris wiped dimension storage at {}", storageFolder.getAbsolutePath());
}
private static void deleteRecursively(Path root) {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.deleteIfExists(path);
}
} catch (IOException e) {
LOGGER.error("Iris failed to wipe dimension storage at {}", root, e);
}
}
}
@@ -0,0 +1,216 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.command.ModdedGuiHost;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedPregenBossBar;
import art.arcane.iris.modded.command.ModdedPregenJob;
import art.arcane.iris.modded.command.ModdedStudioCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ModdedEngineBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String[] CORE_SELF_TEST_CLASSES = {
"art.arcane.iris.engine.IrisEngine",
"art.arcane.iris.util.common.data.B",
"art.arcane.iris.core.loader.IrisData"
};
private static final Object LOCK = new Object();
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform;
private static volatile MinecraftServer currentServer;
private ModdedEngineBootstrap() {
}
public static ModdedServiceManager services() {
return SERVICE_MANAGER;
}
public static ModdedScheduler schedulerOrNull() {
ModdedPlatform bound = platform;
return bound == null ? null : bound.moddedScheduler();
}
public static void tick(MinecraftServer server) {
ModdedScheduler.tick(server);
ModdedStartup.runOnce(server);
ModdedPrimaryWorldRouter.tick(server);
SERVICE_MANAGER.tick(server);
ModdedPregenBossBar.tick(server);
ModdedProtocolHandler.tickDimensionSync(server);
}
public static void start(MinecraftServer server) {
currentServer = server;
bind();
ModdedStartup.reset();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
}
IrisModdedChunkGenerator.startGenPool();
SERVICE_MANAGER.enableAll();
ModdedProtocolHandler.start(server);
ModdedSentry.start(loader());
}
public static void stop() {
ModdedProtocolHandler.stop();
ModdedPregenJob.shutdown();
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
ModdedDimensionManager.clear();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.shutdown();
}
IrisModdedChunkGenerator.shutdownGenPool();
ModdedSentry.flush();
ModdedStartup.reset();
currentServer = null;
}
public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) {
loader = moddedLoader;
ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")");
selfTest(moddedLoader.getClass().getClassLoader());
bind();
MainWorldService.reconcileEarly();
chunkGeneratorRegistration.run();
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
armParityProbe();
armWorldCheck();
}
private static void armParityProbe() {
String parity = System.getProperty("iris.parity");
if (parity == null) {
return;
}
ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity);
}
private static void armWorldCheck() {
if (System.getProperty("iris.worldcheck") == null) {
return;
}
ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule();
}
public static ModdedLoader loader() {
ModdedLoader bound = loader;
if (bound == null) {
throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.bootCommon first");
}
return bound;
}
public static MinecraftServer currentServer() {
MinecraftServer tracked = currentServer;
return tracked != null ? tracked : loader().currentServer();
}
private static void selfTest(ClassLoader classLoader) {
int loadedClasses = 0;
for (String className : CORE_SELF_TEST_CLASSES) {
try {
Class.forName(className, true, classLoader);
loadedClasses++;
} catch (Throwable error) {
LOGGER.error("Iris core self-test failed to initialize {}", className, error);
}
}
if (loadedClasses != CORE_SELF_TEST_CLASSES.length) {
throw new IllegalStateException("Iris core self-test failed: only " + loadedClasses + " of " + CORE_SELF_TEST_CLASSES.length + " engine classes initialized");
}
ModdedIrisLog.info("Iris core loaded (" + loadedClasses + " classes ok)");
}
public static ModdedPlatform bind() {
ModdedPlatform bound = platform;
if (bound != null) {
return bound;
}
synchronized (LOCK) {
if (platform != null) {
return platform;
}
ModdedLoader boundLoader = loader();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
ModdedPlatform created = new ModdedPlatform(boundLoader);
IrisPlatforms.bind(created);
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
ModdedGuiHost.install();
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
if (boundLoader.clientEnvironment()) {
ModdedStartup.prefetchDefaultPack();
}
ModdedIrisSplash.print(boundLoader);
return created;
}
}
}

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