Compare commits

..
Author SHA1 Message Date
Brian Neumann-Fopiano c3359e0a16 Fixing a handfull of issues! 2026-10-21 18:07:10 -04:00
Brian Neumann-Fopiano 1132242c58 Reduced verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover large, short, and empty entries
2026-10-21 09:08:00 -04:00
Brian Neumann-Fopiano 43cb336b85 Reviewed verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover mixed entry sizes
2026-10-21 09:06:24 -04:00
Brian Neumann-Fopiano dc1c90f63f Reduced verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover large, short, and empty entries
2026-10-21 09:06:24 -04:00
Brian Neumann-Fopiano 463feea60b Improved Iris build scheduling
- Resolved SlimJar only for shaded core artifacts
- Applied bounded parallel Forge test execution
2026-10-21 08:27:49 -04:00
Brian Neumann-Fopiano 49f7d11442 Reviewed Iris build scheduling
- Combined dependency task wiring and Forge test limits
2026-10-21 08:25:11 -04:00
Brian Neumann-Fopiano 0eb661f14d Trimmed unused build work
- Limited SlimJar generation to shaded core artifacts
- Honored configured mod test fork limits
- Set Forge targets to four test JVMs
2026-10-21 08:24:55 -04:00
Brian Neumann-Fopiano 5e02c5efdd Forwarded nested dependency settings
- Nested mod builds inherit the Paperweight source generation property
2026-10-21 08:00:04 -04:00
Brian Neumann-Fopiano 8dea0ae3e7 Reviewed dependency source forwarding
- Mod builds retain source generation settings across nested Gradle invocations
2026-10-21 07:58:12 -04:00
Brian Neumann-Fopiano b669e04acf Forwarded dependency source settings
- Nested mod builds inherit the Paperweight source generation property
2026-10-21 07:57:41 -04:00
Brian Neumann-Fopiano 294598d5e1 Improved isolated Forge builds
- Mod verification uses its included core build without duplicate prerequisites
- Nested sandbox builds divide their reserved workers across matrix lanes
- Forge targets reserve eight workers and 16 GiB with two test forks
2026-10-21 07:18:03 -04:00
Brian Neumann-Fopiano 79f5d02a7d Reviewed Forge build budgets
- Divided nested build workers across sandbox matrix lanes
- Reserved eight workers and 16 GiB for Forge verification
2026-10-21 07:11:10 -04:00
Brian Neumann-Fopiano 826a752437 Expanded Forge build budgets
- Sandbox nested builds divide reserved workers across matrix lanes
- Sandbox nested JVM processor counts follow each lane worker budget
- Forge targets reserve eight workers and 16 GiB with two test forks
2026-10-21 07:08:26 -04:00
Brian Neumann-Fopiano 389c2c9c42 Reviewed modded prerequisites
- Modded targets build core jars and fixtures in their own included build
2026-10-21 06:46:34 -04:00
Brian Neumann-Fopiano e1ff5447ca Removed duplicate core prerequisite
- Modded builds resolve core jars and test fixtures through their included build
- Forge setup starts without waiting for unused outer core outputs
2026-10-21 06:43:07 -04:00
Brian Neumann-Fopiano 96903e9cf3 Expanded Iris test parallelism
- Bukkit builds allocate eight workers for the complete core test suite
- Bukkit builds reserve 16 GiB for concurrent test JVMs
2026-10-21 06:34:44 -04:00
Brian Neumann-Fopiano e68c93c0f1 Improved isolated Iris builds
- Bukkit builds allocate four workers and 8 GiB for tests
- Forge sandbox builds retain their private Minecraft dependency cache
2026-10-21 06:22:45 -04:00
Brian Neumann-Fopiano 97da131884 Reviewed Forge cache persistence
- Isolated Forge targets retain generated Minecraft dependencies in their private Gradle homes
2026-10-21 06:22:45 -04:00
Brian Neumann-Fopiano 20ff08aeb7 Persisted Forge build caches
- Sandboxed Forge lanes retain Mavenizer and ForgeGradle caches in their private Gradle home
- Nested mod builds receive the sandbox property
2026-10-21 06:22:20 -04:00
Brian Neumann-Fopiano 9add1a0fa6 Reviewed Bukkit build allocation
- Bukkit build targets reserve four workers and 8 GiB for the complete test suite
2026-10-21 06:18:41 -04:00
Brian Neumann-Fopiano 1b80f4055b Raised Bukkit build budget
- Bukkit source builds use four Gradle workers
- Bukkit source builds reserve 8 GiB of memory
2026-10-21 06:14:01 -04:00
Brian Neumann-Fopiano 365d6f2810 Merged project build improvements
- Reviewed build controls and validation changes are integrated locally
2026-10-21 05:33:21 -04:00
Brian Neumann-Fopiano c17c8bece1 Reviewed sandbox source stamps
- Source stamp changes are integrated for local review
2026-10-21 05:32:24 -04:00
Brian Neumann-Fopiano d84050baac Stabilized sandbox source stamps
- Build constants use the captured Iris source fingerprint
- Unrelated project changes preserve compiled Iris cache inputs
2026-10-21 05:32:24 -04:00
Brian Neumann-Fopiano 7fb684faf5 Merged sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:57 -04:00
Brian Neumann-Fopiano 1342fa6a06 Reviewed sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:57 -04:00
Brian Neumann-Fopiano 52f1a18a1b Declared sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:18 -04:00
Brian Neumann-Fopiano fc810221bb Parallelized mod builds
- Mod targets run through bounded Gradle workers with separate caches and daemon slots
- Target logs retain verification failures and bounded output summaries
- Nested tests use bounded forks and JVM thread counts
- Lifecycle tests control deadlines through injected clocks
2026-10-21 02:53:00 -04:00
Brian Neumann-Fopiano d7059d84a4 V+ 2026-10-20 17:22:54 -04:00
Brian Neumann-Fopiano b357eee181 Fixed runtime configuration
- Timeout overrides retain original values for restoration
- Timeout restoration preserves operator changes and symbolic links
- Tests cover timeout persistence and failed configuration writes
- Runtime targets use separate Gradle caches and shared module outputs
- Modded build inputs include test fixtures and runtime cache settings
2026-10-18 18:15:46 -04:00
Brian Neumann-Fopiano 64f7a7a9ac Merged generation fixes
- Procedural trees keep compact terrain-supported roots
- Canopies preserve trunks across chunk generation and replay
- CraftEngine content follows native terrain replacement and regeneration
- Regression fixtures cover tree geometry and custom content cleanup
2026-10-18 18:04:40 -04:00
Brian Neumann-Fopiano a6aeceff86 Grounded procedural trees
- Compact connected roots extend bounded gaps into supporting terrain
- Unsupported cliff placements leave no partial trees
- Root and canopy roles follow object scaling and rotation
- Canopies preserve solid trunk cells across chunk replay and saved continuations
- Branch wood replaces leaf clusters within each tree
- Regression tests cover slopes and overlapping tree placements
- Gameplay checks compare grounded spindle geometry across both packs
2026-10-18 18:00:40 -04:00
Brian Neumann-Fopiano 51c626c410 Merged custom content fixes
- Deferred placement guards and generated content ownership share the current engine
- Native terrain writes invalidate stale custom content across platforms
2026-10-18 17:59:20 -04:00
Brian Neumann-Fopiano 34417b2f5b Tracked generated content ownership
- Generated furniture keeps ownership tokens for targeted removal
- Native writes invalidate pending custom content and tile records
- Regeneration and chunk updates remove invalidated content on owning threads
- Provider callbacks return ownership tokens across server platforms
- Regression fixtures cover terrain replacement and restart cleanup
2026-10-18 17:58:30 -04:00
SwiftSwamp c3a9449a02 Archive
- Archiving old media to the volmit media repo
2026-10-17 11:43:24 -06:00
Brian Neumann-Fopiano 1076ab47a7 Merge remote-tracking branch 'origin/master' 2026-10-17 11:27:09 -04:00
Brian Neumann-Fopiano 032a8ba46a V+ 2026-10-17 11:03:58 -04:00
SwiftSwamp c7e4b94bf6 Issue Templates
- Updating Issue Templates
2026-10-17 07:15:52 -06:00
Brian Neumann-Fopiano ed5483d75a Merged stale placement guard
- Combine the carrier check with native write invalidation
2026-10-16 19:37:15 -04:00
Brian Neumann-Fopiano ff537f4abc Cleared records under native writes
- Added DeferredPlacementRecords to gate tracking on Identifier and TileWrapper slices
- Cleared deferred custom and tile records at positions native structure placement changed
- Implemented block change tracking in the Bukkit and modded structure policies
- Covered invalidation through the custom block materialization pass
- Covered modded wrapper change tracking and protected cells
- Passed disabled change sets in existing wrapper tests
2026-10-16 19:36:35 -04:00
Brian Neumann-Fopiano dbc9d95372 Skip stale deferred placements
- Skip deferred custom block records whose live block no longer matches the provider carrier type
- Leave air-carrier furniture and unresolvable carriers to the provider unchanged
- Resolve each carrier once per chunk pass and log skipped records once per chunk at debug level
- Skip tile records quietly when the live block no longer matches the tile material
- Apply the same carrier check to the modded custom block pass
- Add Bukkit, CraftEngine and modded regression tests for flattened, replaced, intact and furniture records
2026-10-16 19:32:01 -04:00
Brian Neumann-Fopiano a3eccd8a65 Merged current master
- Bring in master commits made during the parity work
2026-10-16 14:30:29 -04:00
Brian Neumann-Fopiano 413d7c4eb2 Documented mod target builds
- README lists the per-target mod jar tasks and checkAll
2026-10-16 12:14:03 -04:00
Brian Neumann-Fopiano ea9f7a79fa Tested 1.21.1 saved dimensions
- Unit test for the 1.21.1 saved-dimension rule: replaced stems keep their saved generator, Iris worlds and new dimensions persist as they run
- The mixin audit test lists the two 1.21.1 persistence mixins as deliberately unaudited
2026-10-16 12:11:25 -04:00
Brian Neumann-Fopiano 64e4c82716 Named malformed history manifests
- Truncated or malformed generation manifests and epoch metadata report as invalid history again after the strict reader change
- The datapack metadata test expects the range and supported_formats to start at the 1.21.1 format
2026-10-16 12:07:36 -04:00
Brian Neumann-Fopiano 835ad2f704 Merge remote-tracking branch 'origin/master' 2026-10-16 11:38:20 -04:00
Brian Neumann-Fopiano 478fd4d350 Built every mod target
- verify<Loader><version> builds, tests and verifies each Fabric, Forge and NeoForge jar for every mod target
- build<Loader><version> exports each verified jar to PluginOuts, removing stale jars of the same loader and version
- buildAllToOut exports the Bukkit jar and all mod target jars; checkAll runs every project check plus the full mod matrix
- Targets list their unit test loaders; Forge 1.21.1 compiles its tests only because Forge 52 cannot bootstrap Minecraft outside FML
- Removed the single-target fabricJar, forgeJar, neoforgeJar, moddedTest and artifact verification tasks
2026-10-16 11:21:01 -04:00
Brian Neumann-Fopiano 97a3f7e7c3 Ran tests under NeoForge 1.21.1
- Classpath scans accept the union filesystem the NeoForge 21.1 test harness mounts and skip test classes merged into it
- The 1.21.1 NeoForge server access test checks the cached world array the tick loop reads
2026-10-16 11:21:01 -04:00
Brian Neumann-Fopiano 8917bdc974 Read 1.21.1 dimension contract
- The world check derives the End dragon fight flag from the dimension effects when the runtime dimension type has no has_ender_dragon_fight field
2026-10-16 11:21:01 -04:00
Brian Neumann-Fopiano 4daae925c6 Added 26.1.2 and 26.2 mod targets
- Mod targets 26.2 and 26.1.2 (inheriting 26.2) with Fabric 0.19.5, Forge 65.1.3 and 64.1.4, NeoForge 26.2.0.89 and 26.1.2.116
- The 26.3 target reads the minecraft26_3 native access rules
- Per-version pack.mcmeta ranges for 26.2 and 26.1.2
- 26.2 and 26.1.2 test overlays and replacements for the pre-26.3 biome, spawn, carver and sign APIs
- NeoForge compiles against the loader's own ASM while the mod jar keeps the bundled ASM
2026-10-16 11:07:09 -04:00
Brian Neumann-Fopiano 0073002b57 Isolated datapack format test
- The default forced datapack format test unbinds the platform so loader test harnesses that bind a runtime cannot change its result
2026-10-16 11:07:09 -04:00
Brian Neumann-Fopiano 79e7b0c79e Chained mod target overlays
- Mod targets list an ordered overlay chain; later overlays replace earlier ones and earlier source replacements apply to inherited files
- Forge and NeoForge metadata take their loader version range from the target
- The Fabric permissions library is bundled only for targets that declare it
- Dropped the 1.21.1 mods.toml copies that only differed in the loader range
2026-10-16 10:44:12 -04:00
Brian Neumann-Fopiano 03c2630497 Fixed startup lifecycle test
- The levels-ready startup test drives the parallel dimension restore instead of the removed per-dimension restore call
2026-10-16 10:44:12 -04:00
Brian Neumann-Fopiano b8aa00b70e Merged 1.21.1 mod targets
- Merge feature/modded-versions into the modded fix integration
- Take loader pins from gradle/mod-targets.json for every target
- Pin the 26.3 NeoForge target to 26.3.0.83
2026-10-16 10:26:02 -04:00
Brian Neumann-Fopiano 62062c4a95 Added 1.21.1 mod targets
- Build Fabric, Forge and NeoForge per target listed in gradle/mod-targets.json (1.21.1, 26.3)
- Add 1.21.1 adapter source overlays under adapters/*/src/versions/1.21.1
- Add verifyModdedMatrix and per-target Java runtime verification of loader jars
- Re-inject persistent dimensions from the levels-ready hook before the watchdog starts
- Wait for queued chunks and the final save before pregen completes
- Add the 1.21.1 data version and datapack supported_formats range
- Cascade regions whose sea or shore pool loses every biome
- Use Gson calls available on the 1.21.1 runtime
2026-10-16 10:25:19 -04:00
Brian Neumann-Fopiano bf99edce87 Merged fast biome locate
- Biome locate searches the biomes chunks are filled with, filtered by region and biome hints
- Locate checks hydrology only for river targets
2026-10-15 19:27:44 -04:00
Brian Neumann-Fopiano 257c7d4f87 Filtered visible biome locate
- /locate biome on non-stacked Iris dimensions searches the visible biomes chunks are filled with instead of falling back to the vanilla structure-biome search
- BiomeSearchFilter rejects columns whose region, land, sea, shore, cave, carving, subterrain or image-map biomes cannot hold a target, so terrain height is computed only for columns that can match
- Hydrology planning state is consulted only when a target comes from a river policy, so a locate no longer queues hydrology tile plans across its whole radius
- Admitted samples resolve inside their generation-history coordinate scope, so saved-biome reads for generated chunks no longer fail the command
- HintedLocator surface hints and IrisBiome visible key enumeration are shared with the locate filter
- Bukkit and modded biome policies share the same filtered search
2026-10-15 19:04:45 -04:00
Brian Neumann-Fopiano 73bf0d64a5 Merged off-thread world creation
- Modded create, enable and replace-overworld prepare worlds in the background
- Worlds still being created refuse duplicate create, delete, disable and update
2026-10-15 19:01:06 -04:00
Brian Neumann-Fopiano 792f9c2da5 Merged replace target refusals
- Modded /iris replace refuses dimensions no level stem defines
- Replacements count as applied only when the live level generates from Iris
- Unknown replace types list installed packs
2026-10-15 19:01:06 -04:00
SwiftSwamp 0b66b0b1df License Update
- Correcting local license file to match GNU Public v3's actual license.
2026-10-15 13:35:34 -06:00
Brian Neumann-Fopiano 369c864e71 Created modded worlds off-thread
- /iris create, /iris world create, /iris world enable and /iris world replace-overworld acknowledge at once, build the world in the background, and report success or the existing failure messages to the sender when it finishes
- Pack validation, the pack dimension check, generation history creation, pack data loading and the engine build run off the server thread; the server thread only registers the world, creates the level and publishes it
- A second create of a world that is still being created, and delete, disable or update of that world, are refused until it is live
- Server shutdown cancels in-flight creations, waits for an engine that is still building, and rolls back its level and registry entry
- Startup restore and world creation share one dimension injection orchestrator, and abandoning an injection also closes an engine that was already built
- Added the creating-in-background message to every locale file
2026-10-15 12:53:58 -04:00
Brian Neumann-Fopiano 31ec5a83c2 Refused runtime replace targets
- Refused /iris replace for dimensions that no level stem defines, including mod levels created while the server starts
- Skipped hand-edited dimensionReplacements entries for such dimensions at startup without moving their data or writing a level stem override
- Derived replaceable dimensions from the recorded worldgen catalog and the applied replacement state
- Recorded a replacement as applied only when the loaded level generates from Iris with the configured pack and seed
- Kept matching Iris generation history without recording it as an applied replacement
- Dropped applied records whose loaded level uses another chunk generator
- Stopped Iris engines bound to replaced levels that a mod swapped out during startup
- Listed installed packs and pack:dimensionKey forms when the replace type is unknown
- Added the refusal and installed-pack messages to every locale
2026-10-15 12:33:47 -04:00
Brian Neumann-Fopiano e1049949c6 Stabilized NeoForge test suite
- Pack validation publication tests pin the registry context instead of reading a bound platform
- NeoForge tests skip the mixin package, which mod-layer tests cannot load
2026-10-15 10:21:07 -04:00
Brian Neumann-Fopiano a9b394ee5d Merged modded dimension replace
- Modded /iris replace stages vanilla, Iris and mod dimension replacements with Paper syntax
- /iris world takeover removed; replacements live in modded.json dimensionReplacements
2026-10-15 10:11:07 -04:00
Brian Neumann-Fopiano a0cf3d79bf Merged off-thread dimension restore
- Saved dimensions prepare in parallel during world load
- Server start only creates levels and publishes engines
2026-10-15 10:11:03 -04:00
Brian Neumann-Fopiano 969ec7fde3 Merged ingest placement caps
- Templates host at most five land or three sea, shore and cave adoptions
- Themed regions only take mod biomes no common region accepts
2026-10-15 10:11:03 -04:00
Brian Neumann-Fopiano b4df23856c Merged modded world listing
- World rows show each world's own seed and saved chunk total
- Rows are labeled by dimension key
2026-10-15 10:11:03 -04:00
Brian Neumann-Fopiano e881e0a33a Merged NeoForge stable target
- NeoForge builds target 26.3.0.83 and require 26.3.0.65-beta or newer
2026-10-15 10:11:03 -04:00
Brian Neumann-Fopiano a7cf2cac9a Unified modded world replace
- Added /iris replace with override and overwrite aliases on Fabric, Forge, and NeoForge using the Paper target, type, and seed arguments
- Resolved main, nether, end, level-name aliases, iris: and bare names, and any loaded mod dimension as replacement targets
- Kept the saved seed when seed is omitted and stored explicit seeds per replaced dimension
- Applied replaced vanilla and mod dimensions through the forced datapack level stem with the configured seed bound at engine creation
- Applied staged Iris world replacements at startup by moving their terrain aside and repointing the dimension registry
- Refused a second staging while a replacement is pending and checked pack environments for vanilla slots
- Added the modded-only off form to cancel a staged replacement or return a replaced dimension to its own generator
- Reset the Overworld spawn and moved returning players out of unsafe positions after an Overworld replacement
- Renamed the takeover reconciler, model, config key, state file, and recovery folder to replacement names
- Removed /iris world takeover and its help entry and localized messages
2026-10-15 10:10:11 -04:00
Brian Neumann-Fopiano 62c610ce76 Restored saved dimensions off-thread
- Saved Iris dimensions are prepared in parallel during world load (server-about-to-start on NeoForge and Forge, server-starting on Fabric), before the server watchdog runs
- Server start only creates the restored levels, builds their engines in parallel, and publishes them, so its wait no longer grows one world at a time
- Each restored dimension opens its generation history once instead of twice and resolves its active pack once instead of three times
- An epoch snapshot whose content matches its validated source pack reuses that validation instead of running the full pack validator for every world
- Level creation reads the configured pack and structure biome keys from data warmed during preparation
- Immutable generation packs feed the prepared hydrology cache identity from the data's memoized fingerprint instead of hashing the pack twice per engine build
- Restore logging reports each dimension's preparation and engine time and how long the server thread waited
2026-10-15 10:08:51 -04:00
Brian Neumann-Fopiano 9a241b85cb Labeled world rows by dimension
- /iris worlds and /iris info label each row with its own dimension key instead of the shared generator identity
- Dropped the redundant world= field now that the row label carries the dimension key
- Rows for worlds whose engine has not started use the same dimension key label
- Listing tests assert per-world row labels, including an unstarted engine row
2026-10-15 09:29:56 -04:00
Brian Neumann-Fopiano 12f30596e5 Targeted NeoForge 26.3.0.83
- NeoForge builds compile against NeoForge 26.3.0.83
- The NeoForge mod requires NeoForge 26.3.0.65-beta or newer, the first build without the cached world array
- NeoForge server access test covers publishing, removing and restoring runtime levels against the live level map
- README platform table and bug template name the current NeoForge target
2026-10-15 09:21:10 -04:00
Brian Neumann-Fopiano 0c9ae9ed4d Fixed modded world listing
- /iris worlds and /iris info report each Iris world's own seed instead of the server level seed
- /iris worlds and /iris info report the world's persisted generated chunk total instead of the per-session counter
- /iris seed reports the Iris world seed on its World seed line
- Added command tests for per-world seeds, persisted chunk totals and the gamemaster seed gate
2026-10-15 09:19:38 -04:00
Brian Neumann-Fopiano 137724157a Kept mod biomes findable
- Themed regions, those at least five times rarer than the dimension's median region, no longer receive adopted biomes while a common region accepts the biome's role
- Themed regions still take a mod biome when no common region has a pool for its role or every common template is full
- Category affinity now chooses among common regions, so a rare biome is no longer locked inside a region that can be absent for thousands of blocks
2026-10-15 09:14:20 -04:00
Brian Neumann-Fopiano 523cc7e7be Capped adoptions per template
- Each pack biome now serves as the template for at most five adopted land biomes and three adopted sea, shore, or cave biomes
- A mod biome whose best template is full falls back to the next best template under the cap
- A mod biome that finds every compatible template full is skipped with a reason naming the cap instead of being dropped
2026-10-15 09:13:55 -04:00
Brian Neumann-Fopiano 0ca33068f3 Discounted occupied flower columns
- Converted patch decorators also lose the share of surface columns that earlier surface patches in the declaring biomes already occupy, estimated from those patches' expected coverage in the catalog
- Canopy and surface occupancy are combined into one vegetation occlusion estimate per decorated feature
2026-10-14 22:49:45 -04:00
Brian Neumann-Fopiano 4d79d5ed5b Matched regions by biome category
- For land biomes with known categories, the region climate tie-breaker averages only the region's templates that share a category with the mod biome, so a mixed region no longer looks temperate on average
- A small region affinity term favors regions whose templates share the mod biome's categories
2026-10-14 22:46:18 -04:00
Brian Neumann-Fopiano ad66538d7a Kept mod trees on plantable ground
- A mod biome that brings its own vegetation pays a template penalty scaled by the share of the template's top surface layer that cannot hold plants, so snow, ice, sand, and stone templates no longer swallow its trees
- The top layer palette is resolved through biome-palette snippets and weighted by palette entry weight
- Mod biomes without their own vegetation keep the closest climate template
2026-10-14 22:44:16 -04:00
Brian Neumann-Fopiano d749633829 Routed cave biome ores
- Ores declared only by adopted cave biomes are written to the deposits of the region each cave biome joined, scoped to those cave biomes, instead of to the cave biome itself where the deposit stage never reads them
- Ores of adopted surface biomes still go to the biome
2026-10-14 22:38:42 -04:00
Brian Neumann-Fopiano 965e19801d Ordered converted ore hosts
- The worldgen catalog (format 2) records the members of every block tag referenced by a captured placed feature, so ore targets using mod block tags convert instead of passing through
- Ore conversion supports noise_based_count and noise_threshold_count placement counts
- Converted ores that produce another converted ore's host block are written ahead of it, so an ore whose host comes from a sibling ore finds that host during the deposit stage
2026-10-14 22:37:28 -04:00
Brian Neumann-Fopiano d5796b275e Audited datapack priority mixin
- The modded mixin audit checks that the pack selection mixin keeping the Iris forced datapack last is applied
- Ordering tests cover a world datapack saved after the Iris pack and already-ordered selections
2026-10-14 21:41:41 -04:00
Brian Neumann-Fopiano c9786f2d23 Calibrated converted decorator density
- Patch decorators converted from features that snap to a leaf-blocked heightmap lose the share of placements that land on tree canopy in the declaring biomes
- Canopy is estimated from the tree-like features that run before the decoration in each declaring biome's vegetal step, using their expected per-chunk count and a typical crown footprint
- Patches that snap below leaves or run before the trees keep their full density
2026-10-14 21:38:00 -04:00
Brian Neumann-Fopiano 7767d6d30a Scoped mod trees per biome
- importedFeatures gains biomeRules, which deny placed feature families or exact keys only in registry biomes matched by key, namespace, or tag; Iris custom biomes match through their vanilla derivative
- Mod ingest classifies tree-like passthrough features from the catalog feature JSON, including trees nested in random selectors, fallen trees, and huge mushrooms
- Tree-like mod features that a mod injects into vanilla biomes the pack already uses are denied in minecraft biomes, so they no longer double the pack's own trees while adopted mod biomes keep them
- Studio schema digest updated for the new rule shape
2026-10-14 21:35:38 -04:00
Brian Neumann-Fopiano 0bc235afb6 Scored ingest biome placement
- Mod biomes pick their template across all regions by climate distance, biome category agreement from minecraft and c tags, a reuse penalty that spreads biomes over equally good templates, and the region's overall climate
- Regions much rarer than the dimension's median region are treated as themed and only receive a mod biome when nothing else fits
- Templates derived from void biomes no longer act as climate references
- /iris ingest mods accepts excludeRegions=<a,b> to keep mod biomes out of named regions; unknown region names are refused
2026-10-14 21:26:22 -04:00
Brian Neumann-Fopiano 8388194129 Trimmed quiet ingest namespaces
- The mod ingest summary and manifest namespace list only include namespaces that produced at least one ingest item, so mods without worldgen content no longer print empty summary lines
2026-10-14 21:19:06 -04:00
Brian Neumann-Fopiano 506e545edf Placed converted ores in hosts
- Ores converted by mod ingest use the FULL_HEIGHT deposit placement scope, so they replace their host blocks anywhere in their height band like vanilla ore features instead of being culled above the column surface
2026-10-14 21:16:48 -04:00
Brian Neumann-Fopiano aedef3c38a Limited borrowed types to modded
- Pack validation rejects borrowedDimensionType on platforms that generate their own Iris dimension type, with an error naming the supported loaders
- Dimension type keys and registry contracts honor borrowedDimensionType only when the platform generation registry supports borrowed types, so Bukkit always derives its generated iris type
- The Bukkit runtime contract always expects the generated Iris type; hotload still refuses a borrowedDimensionType change
2026-10-14 21:10:25 -04:00
Brian Neumann-Fopiano 9481a9ecf0 Reserved Iris ingest namespaces
- Mod ingest never selects the irisworldgen namespace or a namespace equal to a dimension key of the source pack or any installed Iris pack, so Iris's own scoped and legacy alias biomes are not adopted
- Explicitly requesting one of those namespaces is refused
- A namespace that belongs to a loaded mod is never treated as an Iris dimension alias namespace
2026-10-14 21:08:23 -04:00
Brian Neumann-Fopiano b8aabd4fd1 Fixed ingested ore anchors
- above_bottom and below_top ore anchors resolve against the pack's own dimension height, or the borrowed dimension type's height when the pack borrows one, instead of the vanilla dimension type
- Converted deposit heights now match the pack-relative rebase, so a 0..256 pack no longer receives out-of-world ore bands
2026-10-14 21:07:10 -04:00
Brian Neumann-Fopiano a3f607e264 Added exact imported feature denial
- importedFeatures gains disabledExact, which denies normalized complete placed feature keys without matching key families
- Mod ingest puts converted feature keys in disabledExact, so converting mod:ore_tin no longer suppresses mod:ore_tin_large
- Mod ingest keeps vanilla and unselected mod features generating when the source pack already ran importedFeatures, and otherwise disables vanilla plus every unselected mod namespace that has placed features
- Studio schema digest updated for the new field
2026-10-14 21:05:27 -04:00
Brian Neumann-Fopiano 191d9e88e6 Guarded ingest pack replacement
- Mod ingest compares source and target by real path and file identity, so a differently cased target on a case-insensitive filesystem can no longer resolve to and delete the source pack
- replace=true only replaces a target that contains ingest/manifest.json
- Ingest builds into a hidden staging directory and swaps it in atomically, deleting the previous output only after the new pack is complete
2026-10-14 21:02:29 -04:00
Brian Neumann-Fopiano 3ba6d5b9c1 Contained catalog capture errors
- A worldgen catalog capture that fails with any throwable, during the server-thread scan or the background encode, always completes its future and frees the next capture
- Errors are logged with their stacktrace and no longer escape server startup; only virtual machine errors are rethrown after cleanup
2026-10-14 21:01:08 -04:00
Brian Neumann-Fopiano 56ba2e51b7 Preserved merged tag order
- Merged datapack tags keep first-seen value order with duplicates removed instead of re-sorting, so world preset tag order is preserved
- NeoForge remove lists are kept as the deduplicated union of both tags instead of being dropped
2026-10-14 21:00:02 -04:00
Brian Neumann-Fopiano 038833e614 Refused unstageable takeovers
- Datapack staging refuses a takeover it cannot stage when the level has applied takeover state or Iris generation history, instead of silently leaving the mod generator in place
- A level's retained generation history now stages its takeover even when the installed pack is gone
- Takeover pack names and dimension keys are resolved to the installed folder and file names before staging, reconciliation, and recording, and the takeover command stores that form
- Reconciliation refuses to switch an applied level to an uninstalled pack before moving any data, and leaves a never-applied level untouched until its pack is installed
2026-10-14 20:59:09 -04:00
Brian Neumann-Fopiano cf0587c112 Hardened takeover state recording
- A changed level whose existing Iris generation history already uses the configured pack is recorded as applied instead of quarantined
- New and changed takeovers are recorded only once their level generates from the configured pack; removals are recorded at reconciliation
- A changed takeover keeps its previous applied state until the new pack generates, so a later staging failure still sees the level as taken over
- Failing to write the applied takeover state stops the server instead of only logging
2026-10-14 20:54:57 -04:00
Brian Neumann-Fopiano a0c9d48601 Derived retained borrowed pack names
- Retained borrowed-type packs take their pack name from the scoped biome aliases recorded in their generation history instead of the dimension key
- Scoped legacy biome aliases of retained borrowed packs now match the definitions their worlds reference
- A retained pack whose recorded aliases name several packs is refused instead of staged with a guessed name
2026-10-14 20:53:08 -04:00
Brian Neumann-Fopiano 4244aa4ea7 Failed closed on unreadable takeovers
- Modded config reports unparseable files, non-object takeover sections, and invalid takeover entries instead of returning silent defaults
- Takeover reconciliation refuses startup when takeovers are unreadable and the world has an applied takeover state file
- Config writes are refused while takeovers are unreadable so fallback defaults never overwrite the operator's file
2026-10-14 20:51:18 -04:00
Brian Neumann-Fopiano 79e220298f Merged dimension takeover 2026-10-14 20:19:44 -04:00
Brian Neumann-Fopiano e1393e2516 Merged worldgen catalog 2026-10-14 20:19:27 -04:00
Brian Neumann-Fopiano acec550625 Merged forced datapack tags
- Staged packs that write the same data/*/tags file now merge into the union of their values
- Tag entries dedupe by value or id and replace stays false unless a contributor set it
- Every non-tag file keeps strict conflict detection
2026-10-14 20:19:04 -04:00
Brian Neumann-Fopiano 769e077078 Clarified ingest target errors
- Rejected target pack names outside letters, digits, underscores and hyphens before ingest starts
- Reported an existing target pack with a translated replace=true hint instead of the raw path
2026-10-14 20:18:50 -04:00
Brian Neumann-Fopiano 5633eec009 Added dimension takeover
- IrisDimension borrowedDimensionType reuses an existing registered dimension type instead of generating one
- Pack validation rejects borrowedDimensionType values that are not namespaced registry keys
- Generation contracts record the borrowed type key and capture it as an external registry definition
- Borrowed types bind when the pack min equals the type min_y and the pack top fits, with both ranges in the refusal
- Studio hotload rejects borrowed type changes and the world check validates borrowed ranges
- Forced datapack skips generated types and presets for borrowed dimensions and accepts retained borrowed epochs
- modded.json dimensionTakeovers writes a LevelStem per level key using the irisworldgen:iris generator
- Takeover changes quarantine the level chunk, entity, POI, and Iris data before load and track applied state per world
- Fresh levels adopt generation history with content-addressed biomes instead of legacy aliases
- /iris world takeover <dimension> <pack[:dimension]|off> with help and translations in every locale
2026-10-14 20:16:36 -04:00
Brian Neumann-Fopiano 7fc8d20516 Merged mod ingester 2026-10-14 20:14:16 -04:00
Brian Neumann-Fopiano f6f381f6e4 Added modded worldgen catalog
- Captured the worldgen catalog after server start, scanning registries on the server thread and encoding and writing on a worker into config/irisworldgen/catalog/worldgen-catalog.json
- Added WorldgenCatalogFingerprint over namespace versions and registry keys
- Added /iris catalog [namespace|refresh] and /iris ingest mods <source> <target> with keyed dimension, namespaces and replace options
- Added catalog messages and help entries for every shipped locale
- Added tests for catalog assembly, namespace summaries, ingest arguments, the registry scan and command registration
2026-10-14 20:14:11 -04:00
Brian Neumann-Fopiano 665c6df83d Merged mod ingester 2026-10-14 20:12:47 -04:00
Brian Neumann-Fopiano 26dc1d17cf Implemented mod ingest v0
- ModIngester copies the source pack with IrisProjectCopier into a new target pack whose folder name becomes the dimension key, honouring replaceTarget and rejecting overlapping or already-ingested sources
- Mod biomes are classified by minecraft/c tags into land, sea, shore, and cave pools, skipping rivers, other-dimension biomes, and biomes missing the dimension tag
- Each adopted biome joins the region with the nearest land climate and copies the nearest-climate template biome with the mod biome as its derivative and no custom-biome overrides
- Template trees and procedural trees are dropped when the mod biome brings its own non-decorator vegetation
- Ore and scattered ore features become Iris deposits per target with resolved height anchors, counts, rarity, vein size, air-exposure discard, and host blocks
- Vanilla-declared ores become dimension deposits filtered to the declaring biomes when they do not cover the whole dimension; mod-only ores become biome deposits
- Surface simple_block and legacy random_patch features become Iris decorators on adopted mod biomes and on pack biomes whose derivative declares them
- Remaining features pass through native generation, with importedFeatures enabled and minecraft plus every converted key disabled
- Passthrough features shadowed by a disabled prefix or a disabled step are reported as skipped
- Mod structures are reported as adopted unless importedStructures disables them or none of their biomes generate
- The target pack records ingest/manifest.json with the catalog fingerprint, namespaces, generated and modified files, and every item
- Tests cover climate placement, biome classification, ore and patch conversion against current 26.3 formats, propagation, passthrough, manifest, idempotent replace, and pack validation
2026-10-14 20:02:37 -04:00
Brian Neumann-Fopiano 8afb445b1d Added worldgen catalog model
- Added the platform-neutral worldgen catalog records, index and atomic JSON store
- Added the mod ingest request, report, item and tier types
2026-10-14 19:26:03 -04:00
Brian Neumann-Fopiano 3246e88f88 Merged treasure loot fixes
- Failed container fills retain pending work for retry
- Loot events and inventory writes run on the owning thread
- Native loot tables resolve against the current registry
- Gameplay checks cover map destinations and copper containers
2026-10-13 17:00:43 -04:00
Brian Neumann-Fopiano 09d36ec990 Removed temporary movement trace
- Gameplay retains tick delimiters without intercepting packet writes
- Chest outcome and failure phase evidence remain in reports
2026-10-13 16:58:45 -04:00
Brian Neumann-Fopiano 718b13df46 Paced treasure chest interactions
- Chest activations respect the server interaction rate limit
- Stationary clients finish idle ticks every fifty milliseconds
- Missing inventory windows fail within thirty seconds
2026-10-13 16:57:30 -04:00
Brian Neumann-Fopiano f17a9e0977 Bounded treasure inventory actions
- Chest activation and diamond withdrawal stop after ten seconds
- Partial chest evidence records the active phase and window layout
- Folia reports its unavailable reload command without changing pack files
2026-10-13 16:55:00 -04:00
Brian Neumann-Fopiano 9dbb6b6e85 Completed fixture client ticks
- Stationary gameplay sends tick delimiters after movement packets
- An initial delimiter closes movement sent before the scenario starts
2026-10-13 16:52:12 -04:00
Brian Neumann-Fopiano ee3af26f11 Captured fixture movement packets
- Gameplay reports the last 64 movement packets with player position and rotation
- Non-finite packet fields remain visible in failed scenario reports
2026-10-13 16:49:03 -04:00
Brian Neumann-Fopiano 13171edafc Merged treasure gameplay checks
- Gameplay fixtures verify map destinations and retained container loot on Paper and Folia
2026-10-13 16:42:57 -04:00
Brian Neumann-Fopiano ba2ecb7c0f Added treasure loot coverage
- Gameplay opens normal and copper chests before inspecting loot
- Map markers and saved centers match verified structure targets
- Missing and disabled destinations retain unrelated chest loot
- Storage variants, insertion rollback, region ownership, and resource reloads have runtime assertions
- Separate data packs cover Minecraft 26.2 and 26.3 loot formats
2026-10-13 16:42:41 -04:00
Brian Neumann-Fopiano 57882a3f1c Merged container loot fixes
- Container loot rolls retain retry state and restore contents after failed writes
2026-10-13 16:40:04 -04:00
Brian Neumann-Fopiano 56178b0e0a Fixed loot materialization
- Container loot finishes on its owning thread before pending markers clear
- Failed fills restore container contents and deferred native loot state
- Failed materialization remains pending without refilling completed containers
- Vanilla tables resolve from the current registry after reload
- Studio loot previews follow the player scheduler
- Tests cover loot rollback, retries, cancellation, and container classification
2026-10-13 16:39:39 -04:00
Brian Neumann-Fopiano aa3420b4fa Merged terrain sampling
- Concurrent fallback queries share pending terrain samples
- Pure height generators reuse noise contexts across batches
- Storage allocation tests account for the configured heap budget
2026-10-13 15:20:40 -04:00
Brian Neumann-Fopiano cec9568c9a Stabilized allocation limit test
- Prepared tile rejection coverage scales with the heap budget using a small shared fixture
2026-10-13 15:09:39 -04:00
Brian Neumann-Fopiano 5b5f3dae89 Reviewed terrain cache coordination
- Concurrent terrain requests share completion and coordinate claims per batch
2026-10-13 14:58:00 -04:00
Brian Neumann-Fopiano cb7eea604e Reviewed bulk terrain sampling
- Pure terrain generators now support ordered bulk height evaluation
- Column cache batches retain scalar sample values
2026-10-13 14:57:58 -04:00
Brian Neumann-Fopiano a207cdd828 Batched terrain cache coordination
- Cold terrain batches share one completion signal across their owned samples
- Cache claims and publication acquire each populated stripe once
- Owned sample batches preserve request order across stripes and repeated coordinates
2026-10-13 14:57:44 -04:00
Brian Neumann-Fopiano 0d34fae316 Batched terrain noise
- Hydrology grids carry uncached heights through bounded noise batches
- Pure generator layers resolve seed contexts once per batch
- Surface smoothing and volumetric terrain retain scalar output
- Provisional terrain samples stay out of routing caches
- Tests cover exact parity, scalar fallbacks, batch limits and cached misses
2026-10-13 14:57:12 -04:00
Brian Neumann-Fopiano 2889a2dbc9 Reviewed shared terrain samples
- Fallback trials share pending coordinate samples
- Worker provisional results remain uncached
2026-10-13 14:48:24 -04:00
Brian Neumann-Fopiano baf64fb9d1 Shared pending terrain samples
- Overlapping terrain batches and point requests share pending coordinate samples
- Failed terrain loads release reservations for retry
- Provisional worker results mark every caller and stay out of the shared cache
- Concurrency tests cover overlap, recursion, interruption, failure and single-worker batching
2026-10-13 14:47:41 -04:00
Brian Neumann-Fopiano 0e90d38acb Merged terrain bounds fix
- Shared bounds preserve distant coordinates and interpolator identities
- Cache and caller regressions cover concurrent replacement
2026-10-13 13:57:10 -04:00
Brian Neumann-Fopiano acc2619bed Reviewed distant terrain bounds
- Shared terrain bounds retain complete coordinate and interpolator identities
2026-10-13 13:56:31 -04:00
Brian Neumann-Fopiano 750ee87c6b Fixed distant terrain bounds
- Shared terrain bounds retain full coordinates and interpolator identities
- Cache regressions cover distant corners and concurrent replacement
2026-10-13 13:56:26 -04:00
Brian Neumann-Fopiano d34dd48f74 Merged generation and lifecycle fixes
- Folia retains generation resources until native workers drain
- Release builds record canonical world version metadata
- Cave carving reuses fixed density columns
- Offline probes handle concurrent directory writes
- Worker checks allow bounded hydrology prefetch completion
2026-10-13 12:47:47 -04:00
Brian Neumann-Fopiano 99c7559f5d Extended probe worker drain
- Offline generation checks allow bounded hydrology prefetch completion

- Worker timeout failures include active pool state

- Tests cover timeout and interrupt handling
2026-10-13 12:30:09 -04:00
Brian Neumann-Fopiano 65894df632 Extended probe worker drain
- Probe workers share a bounded 120-second settling deadline

- Timeout failures include the unsettled pool state

- Tests cover active work, deadline expiry, and interruption
2026-10-13 12:29:53 -04:00
Brian Neumann-Fopiano 6ff13d11c3 Fixed probe workspace cleanup
- Probe cleanup handles files created during traversal with bounded rescans
- Filesystem failures remain visible
- Symlink targets remain untouched
2026-10-13 12:28:58 -04:00
Brian Neumann-Fopiano 7d8968c45a Fixed probe cleanup
- Offline probes share bounded directory rescans after concurrent file creation
- Cleanup preserves other I/O failures
- Symlink targets remain untouched
- Probe tests use Mockito to reproduce cleanup races and failure boundaries
2026-10-13 12:28:32 -04:00
Brian Neumann-Fopiano 4df536a6ca Reused cave lattice lookups
- Adaptive cave passes retain exact fixed-density columns across height samples
2026-10-13 12:02:35 -04:00
Brian Neumann-Fopiano 54feb80295 Reused cave density columns
- Cave passes reuse validated lattice columns in local scratch
- Generation releases retained columns after each carve
- Regression tests cover density parity and cache lifetime
2026-10-13 12:01:59 -04:00
Brian Neumann-Fopiano 4f32c78982 Fixed Folia shutdown ordering
- Queued generation remains available through native world shutdown

- Deferred resource release requires the authoritative shutdown boundary
2026-10-13 11:46:03 -04:00
Brian Neumann-Fopiano 7dfcd6a989 Fixed world version metadata
- Fresh worlds retain the packaged Iris release number
2026-10-13 11:45:45 -04:00
Brian Neumann-Fopiano 8c0b37c0b8 Fixed Folia shutdown ordering
- Server shutdown keeps queued terrain generation admitted
- Runtime cleanup waits for the authoritative server boundary
- Missing shutdown boundaries retain engine resources
- Tests cover maintenance admission and active generation drains
2026-10-13 11:45:31 -04:00
Brian Neumann-Fopiano 4b44bcb12b Fixed release version metadata
- Engine metadata accepts unsuffixed Iris release versions
- Invalid release numbers return the existing unknown value
- Version tests cover packaged releases and supported Minecraft formats
2026-10-13 11:43:08 -04:00
Brian Neumann-Fopiano 2e3a41f262 Merged generation optimizations
- Generation skips empty floating work and repeated river lookups
- Height prefill uses batched cache access
- Deep fixed fracture chains reuse exact samples
2026-10-13 11:32:21 -04:00
Brian Neumann-Fopiano 45745abaa1 Merged fracture sampling reuse
- Deep fixed noise chains reuse paired samples with exact output
2026-10-13 11:15:03 -04:00
Brian Neumann-Fopiano ac026343c1 Merged height sampling changes
- Height prefill reuses chunk caches and mapped biomes skip unused sampling

- Shared terrain corner storage allocates only when sampled
2026-10-13 11:13:49 -04:00
Brian Neumann-Fopiano 78ddaaaf53 Reduced height sampling work
- Height prefill reads aligned worker chunks through the bulk cache
- Main-thread heights keep nonblocking hydrology sampling
- Biome maps skip unused procedural fallback samples
- Corner bounds allocate only when terrain samples need them
- Tests cover cache parity and provisional values
2026-10-13 11:12:55 -04:00
Brian Neumann-Fopiano de90d0ec5e Merged floating work reduction
- Empty floating placements skip redundant mantle work
2026-10-13 11:12:15 -04:00
Brian Neumann-Fopiano fac3b4e106 Reused nested fracture samples
- Pure deep fracture chains reuse coordinate pairs within each sample
- Each sample rechecks runtime noise purity
- Tests cover bit parity, sample counts, mutations and concurrent engines
2026-10-13 11:11:07 -04:00
Brian Neumann-Fopiano 8757355174 Removed empty floating work
- Objectless floating placements no longer expand mantle generation windows
- Empty floating object passes finish without column sampling
- Tests cover placement reach, reload, failure fallback and completion flags
2026-10-13 11:11:02 -04:00
Brian Neumann-Fopiano 2af594de61 Merged river lookup batching
- Post processing reuses river-weight samples across neighboring columns
2026-10-13 11:07:22 -04:00
Brian Neumann-Fopiano 6e77755279 Batched river lookups
- Post processing samples each river-weight coordinate once per chunk
- River exclusions preserve positive-weight and cardinal-neighbor rules
- Tests cover boundaries, nonfinite weights, wall patches and slabs
2026-10-13 11:06:33 -04:00
Brian Neumann-Fopiano 12591820e8 Corrected release labels
- Iris release version is 4.3.3
- Loader build defaults use the plain release version
- Platform jar labels retain their supported game versions
2026-10-12 19:33:50 -04:00
Brian Neumann-Fopiano a521109a6a Updated release version
- Iris version is 4.3.3-26.2
- Loader build defaults use the current release version
2026-10-12 14:47:39 -04:00
Brian Neumann-Fopiano a9f5145c0f Fixed Iris artifact builds
- Loader jars exclude Bukkit dependency bootstrap classes
- Artifact verification rejects bootstrap references and executable payloads
- Verifier tests cover nested jars and bundled compression libraries
- Local VolmLib native setup finishes before Iris native setup
2026-10-12 11:34:41 -04:00
Brian Neumann-Fopiano e3673afb1e Fixed mining and timeout settings
- Pending biome reads no longer cancel block breaking
- Custom loot resolves saved biome history after accepted breaks
- Native drops survive failed reads and runtime teardown without duplication
- Tree felling captures the original tool for fallback drops
- Modded block loot resolves one historical biome context
- Timeout settings persist atomically through the actual server configuration paths
- Tests cover deferred loot, teardown and timeout persistence
2026-10-12 09:27:26 -04:00
Brian Neumann-Fopiano 2145e90b1c Rebase 2026-10-11 10:35:59 -04:00
SwiftSwamp d480abad50 Added shared update notices
- Iris joins shared Volmit update checks and reports
- The iris.update permission controls update visibility
- general.updateNotifications defaults to enabled
- Accepted settings changes reconfigure update checks
- Shutdown removes the settings listener and closes update checks
- Tests cover persisted opt-outs and notifier lifecycle
2026-10-09 09:30:12 -06:00
Brian Neumann-Fopiano be0cd567ed V+
- Iris version is 4.3.2-26.2
- Loader build fallbacks use the current release version
2026-10-08 17:17:12 -04:00
Brian Neumann-Fopiano ce7872f667 Removing scheduler
- not needed
2026-10-08 13:14:35 -04:00
543 changed files with 38243 additions and 3132 deletions

No files matched your search

+56 -65
View File
@@ -1,87 +1,78 @@
name: Iris Bug Report
description: File a bug report for Iris
labels: [ bug ]
name: Bug report
description: Something in Iris isn't working.
labels: [bug]
type: Bug
assignees: [SwiftSwamp]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill this out!
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
Make sure you're on the [latest release](https://github.com/VolmitSoftware/Iris/releases/latest) and that it hasn't [already been reported](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue).
The Iris plugin supports Minecraft 26.1.2 through 26.3 on Paper, Purpur, Leaf, Canvas, Folia, and Spigot. The Fabric, Forge, and NeoForge builds support 26.3. Both need Java 25.
- type: textarea
id: how
id: issue
attributes:
label: Problem
description: Please give a text description of how you reached the problem
value: |
1. Install Iris...
2. Do this...
3. Do that...
4. Observe the error...
label: What issue are you having?
description: Include any error message you saw.
validations:
required: true
- type: textarea
id: what
id: steps
attributes:
label: Solution
description: Explain where you think the problem comes from (optional)
placeholder: The code to place a is missing b and c...
label: Steps to reproduce
placeholder: |
1. Start a server with Iris installed
2. Change ... in the config
3. Run /iris ...
4. ... happens instead of ...
validations:
required: false
required: true
- type: input
id: diagnostics
attributes:
label: Debug dump or latest log file
description: Run `/iris debug dump upload=true` and paste the link. If Iris won't start, upload `logs/latest.log` to [mclo.gs](https://mclo.gs) and paste that link instead.
placeholder: https://mclo.gs/...
validations:
required: true
- type: input
id: plugin-version
attributes:
label: "Iris version"
description: Run `/iris debug version` (`/iris version` on Fabric, Forge, and NeoForge).
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
label: Server software
options:
- Paper
- Purpur
- Leaf
- Canvas
- Folia
- Spigot / CraftBukkit
- Fabric
- Forge
- NeoForge
- Other (describe under Problem)
validations:
required: true
- type: dropdown
id: mcversion
attributes:
label: Minecraft Version
description: What version of Minecraft is the server on? Iris 4.x supports 26.1.2 to 26.3 on Bukkit-family servers and 26.3 on Fabric, Forge and NeoForge; older versions are not supported.
options:
- '26.3'
- '26.2'
- '26.1.2'
- "Paper"
- "Purpur"
- "Leaf"
- "Canvas"
- "Folia"
- "Spigot"
- "Fabric"
- "Forge"
- "NeoForge"
- Other (unsupported)
validations:
required: true
- type: input
id: loaderversion
- type: dropdown
id: minecraft
attributes:
label: Platform / Loader Version
description: Exact server or loader build (see console). For example "Paper 26.3-133", "Fabric Loader 0.19.5", "NeoForge 26.3.0.33-beta".
placeholder: DO NOT SAY "LATEST"
label: Minecraft version
options:
- "26.3"
- "26.2"
- "26.1.2"
- Other (unsupported)
validations:
required: true
- type: input
id: irisversion
- type: textarea
id: extra
attributes:
label: Iris Version
description: What version of Iris are you running? (see console)
placeholder: DO NOT SAY "LATEST"
validations:
required: true
- type: input
id: logs
attributes:
label: Log
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
placeholder: https://mslog.gs/...
validations:
required: true
- type: markdown
id: thanks
attributes:
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
label: Anything else?
description: Other plugins involved, screenshots, or videos.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Help and questions
url: https://volmitsoftware.com/discord
about: Ask setup and configuration questions on the Volmit Discord.
- name: "Iris documentation"
url: https://docs.volmitsoftware.com/iris
about: Installation, commands, permissions, and configuration.
+15 -33
View File
@@ -1,46 +1,28 @@
name: Iris Feature Request
description: File a feature request for Iris. If you want to report a bug this is not the place.
labels: [ feature ]
name: Suggestion
description: Suggest a new feature or a change to Iris.
labels: [enhancement]
type: Feature
assignees: [SwiftSwamp]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill this out!
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
- type: dropdown
id: arc
Check the [existing suggestions](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue+label%3Aenhancement) first and add relevant information to the existing issue.
- type: textarea
id: problem
attributes:
label: Adding, Removing, or Changing
description: What are you doing
options:
- Adding
- Removing
- Changing
validations:
required: true
- type: input
id: atype
attributes:
label: Type of Modification
description: What is it for?
value: I want to ...
label: What are your suggestions for new features or changes?
validations:
required: true
- type: textarea
id: desc
id: proposal
attributes:
label: What are you trying to modify
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
value: The way I would implement this is ...
label: How should it work?
description: Provide as much detail as possible on what you would like changed or added.
validations:
required: true
- type: textarea
id: alternative
id: extra
attributes:
label: Alternatives
description: What alternatives have you considered?
value: If this could not be implemented I would ...
- type: markdown
id: thanks
attributes:
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
label: Anything else?
description: Any additional information such as screenshots, examples, etc.
+55
View File
@@ -0,0 +1,55 @@
name: Translation
description: A message in Iris is wrong, missing, or unclear.
labels: [bug]
type: Bug
assignees: [SwiftSwamp]
body:
- type: markdown
attributes:
value: |
The language files are in [`core/src/main/resources/languages/`](https://github.com/VolmitSoftware/Iris/tree/HEAD/core/src/main/resources/languages).
You can create a pull request to modify the files directly.
- type: dropdown
id: language
attributes:
label: Language
options:
- English (en_US)
- German (de_DE)
- Spanish (es_ES)
- Finnish (fi_FI)
- French (fr_FR)
- Hebrew (he_IL)
- Italian (it_IT)
- Japanese (ja-JP)
- Korean (ko_KR)
- Lithuanian (lt_LT)
- Dutch (nl_NL)
- Polish (pl_PL)
- Portuguese (pt_PT)
- Russian (ru_RU)
- Turkish (tr_TR)
- Vietnamese (vi_VI)
- Chinese, Simplified (zh_CN)
- Chinese, Traditional (zh_TW)
- Another language (name it below)
validations:
required: true
- type: textarea
id: current
attributes:
label: Current text
validations:
required: true
- type: textarea
id: suggested
attributes:
label: Suggested text
validations:
required: true
- type: input
id: where
attributes:
label: Where does it appear?
description: The command used or message key where it is appearing.
+452 -296
View File
@@ -1,160 +1,215 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and
that you know you can do these things.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights.
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
responsibilities to respect the freedom of others.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
show them these terms so they know their rights.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute
and/or modify it.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
will not be attributed erroneously to authors of previous versions.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the freedom of users.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "
Licensees" and
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission,
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
work "based on" the earlier work.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the Program.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
countries other activities as well.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction
with a user through a computer network, with no transfer of a copy, is not conveying.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright
notice, and (2)
tells the user that there is no warranty for the work (except to the extent that warranties are provided), that
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
1. Source Code.
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means
any non-source form of a work.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body,
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
working in that language.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
available to the public in source code form. A
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used
to run it.
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (
for an executable work) run the object code and to modify the work, including scripts to control those activities.
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
Source includes interface definition files associated with source files for the work, and the source code for shared
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
communication or control flow between those subprograms and other parts of the work.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the
Corresponding Source.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that same work.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
prohibit them from making any copies of your copyrighted material outside their relationship with you.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not
allowed; section 10 makes it unnecessary.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
restricting circumvention of such measures.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
your or third parties' legal rights to forbid circumvention of technological measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for
a fee.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source
code under the terms of section 4, provided that you also meet all of these conditions:
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
@@ -177,17 +232,22 @@ code under the terms of section 4, provided that you also meet all of these cond
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of
the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or
distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
this License to apply to the other parts of the aggregate.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License, in one of these ways:
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
@@ -230,55 +290,77 @@ machine-readable Corresponding Source under the terms of this License, in one of
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
need not be included in conveying the object code work.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
the product.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information
required to install and execute modified versions of a covered work in that User Product from a modified version of its
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
is in no case prevented or interfered with solely because modification has been made.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
example, the work has been installed in ROM).
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
and adversely affects the operation of the network or violates the rules and protocols for communication across the
network.
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format
that is publicly documented (and with an implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
or can give appropriate copyright permission.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by
the copyright holders of that material) supplement the terms of this License with terms:
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
@@ -303,182 +385,251 @@ the copyright holders of that material) supplement the terms of this License wit
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
that license document, provided that the further restriction does not survive such relicensing or conveying.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as
exceptions; the above requirements apply either way.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
licenses granted under the third paragraph of section 11).
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is
reinstated (a)
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
after the cessation.
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you
of the violation by some reasonable means, this is the first time you have received notice of violation of this
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
notice.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
qualify to receive new licenses for the same material under section 10.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run,
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
parties with this License.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
reasonable efforts.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
selling, offering for sale, or importing the Program or any portion of it.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the
Program is based. The work thus licensed is called the contributor's "contributor version".
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
sublicenses in a manner consistent with the requirements of this License.
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
contributor version.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not
to enforce a patent
(such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
that country that you have reason to believe are valid.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
automatically extended to all recipients of the covered work and works based on it.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of,
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to
infringement that may otherwise be available to you under applicable patent law.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
would be to refrain entirely from conveying the Program.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
to the combination as such.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
problems or concerns.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used,
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
Program.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed
on any author or copyright holder as a result of your choosing to follow a later version.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
in return for a fee.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve
this is to make it free software which everyone can redistribute and change under these terms.
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
to where the full notice is found.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
@@ -498,21 +649,26 @@ to where the full notice is found.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of
course, your program's commands might be different; for a GUI interface, you would use an "about box".
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+17 -19
View File
@@ -25,7 +25,7 @@ Canonical English is defined in the typed Java catalogs under `core/src/main/jav
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.3 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; current target is NeoForge 26.3.0.33-beta |
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; requires NeoForge 26.3.0.65-beta+; current target is NeoForge 26.3.0.83 |
Java 25 is required on every platform.
@@ -248,28 +248,26 @@ which is what a board reader means.
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```sh
./gradlew buildAll
```
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
This builds and verifies the Bukkit plugin, API, SPI and the Fabric, Forge and NeoForge jars
for every Minecraft version declared in `gradle/mod-targets.json`. Artifacts stay in the
producing module's `build/` directory. The packed Bukkit plugin is in `build/libs/`, the
API is in `build/api/`, the SPI is in `spi/build/libs/`, and mod jars are in
`adapters/<loader>/build/<minecraft>/libs/`.
```
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
```
Use `./gradlew buildBukkit` for the plugin, or `verify<Loader><mc>` for one mod jar,
for example `verifyFabric26_2` or `verifyNeoForge1_21_1`. `buildFabric`, `buildForge`
and `buildNeoforge` select the `minecraftVersion` Gradle property. `./gradlew checkAll`
runs every check plus the complete mod matrix. Forge 1.21.1 compiles its tests because
Forge 52 cannot bootstrap Minecraft outside FML.
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
changes only the consumer directory. Both tasks copy all four verified platform jars into
`../PluginOuts/`.
Multiplexor reads `gradle/multiplexor.gradle` to discover targets and build each in a
separate source sandbox. Use its project build and deployment commands to export jars
or install them into managed test instances. Gradle builds do not copy artifacts to
external destinations.
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
int supportY = x < 2 ? groundY - 33 : 12;
for (int y = supportY; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
if (x < 2) {
for (int y = 13; y < supportY; y++) {
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
}
}
}
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
int x = start.getBoundingBox().maxX() + 23;
int z = start.getBoundingBox().minZ();
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = groundY - 35; y <= groundY; y++) {
BlockState block = y >= groundY - 34 && y <= groundY - 13
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
put(blocks, x, y, z, block);
}
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
for (int y = groundY - 34; y <= groundY - 13; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
}
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
}
@Test
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
for (int y = groundY - 11; y <= groundY - 9; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 8; y < groundY; y++) {
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
int supportY = originalFoundation ? 63 : 20;
for (int y = supportY; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
if (originalFoundation) {
for (int y = 21; y < supportY; y++) {
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
true, true,
position -> false,
NativeBlockChangeSet.disabled());
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
false, true,
position::equals,
NativeBlockChangeSet.disabled());
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
false, true,
position -> false,
NativeBlockChangeSet.disabled());
}
private static ChunkPos generationCenter() {
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
StructureTerrainSettings.SOURCE, (x, z) -> 63);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
}
@Test
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
-256, 0, -256, 256, 255, 256, null));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
-256, 0, -256, 256, 100, 256, null));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
@@ -544,7 +548,8 @@ public class NativeStructureVolumeIndexTest {
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
StructureTerrainSettings.SOURCE, (x, z) -> 63);
}
}
@@ -596,7 +601,7 @@ public class NativeStructureVolumeIndexTest {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
15, 255, 15, null));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
int supportY = x < 2 ? groundY - 33 : 12;
for (int y = supportY; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
if (x < 2) {
for (int y = 13; y < supportY; y++) {
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
}
}
}
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
int x = start.getBoundingBox().maxX() + 23;
int z = start.getBoundingBox().minZ();
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = groundY - 35; y <= groundY; y++) {
BlockState block = y >= groundY - 34 && y <= groundY - 13
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
put(blocks, x, y, z, block);
}
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
for (int y = groundY - 34; y <= groundY - 13; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
}
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
}
@Test
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
for (int y = groundY - 11; y <= groundY - 9; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 8; y < groundY; y++) {
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
int supportY = originalFoundation ? 63 : 20;
for (int y = supportY; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
if (originalFoundation) {
for (int y = 21; y < supportY; y++) {
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
true, true,
position -> false,
NativeBlockChangeSet.disabled());
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
false, true,
position::equals,
NativeBlockChangeSet.disabled());
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
false, true,
position -> false,
NativeBlockChangeSet.disabled());
}
private static ChunkPos generationCenter() {
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
StructureTerrainSettings.SOURCE, (x, z) -> 63);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
}
@Test
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
-256, 0, -256, 256, 255, 256, null));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
-256, 0, -256, 256, 100, 256, null));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
@@ -544,7 +548,8 @@ public class NativeStructureVolumeIndexTest {
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
StructureTerrainSettings.SOURCE, (x, z) -> 63);
}
}
@@ -596,7 +601,7 @@ public class NativeStructureVolumeIndexTest {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
15, 255, 15, null));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
@@ -37,6 +37,7 @@ import art.arcane.iris.world.PendingWorldDeleteQueue;
import art.arcane.iris.world.PendingWorldReplacementManager;
import art.arcane.iris.world.WorldRefusalReporter;
import art.arcane.iris.configuration.SettingsHotloadWatch;
import art.arcane.iris.update.IrisUpdateNotifier;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
@@ -132,8 +133,11 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.IllegalPluginAccessException;
@@ -205,6 +209,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private volatile boolean papiLifecycleActive;
private KMap<Class<? extends IrisService>, IrisService> services;
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
@@ -215,9 +220,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
private BukkitLanguageSwitcher languageSwitcher;
private IrisUpdateNotifier updateNotifier;
private BukkitDebugDump debugDump;
private volatile SettingsHotloadWatch settingsHotloadWatch;
private volatile Thread serverLifecycleThread;
private volatile ServerShutdownBoundary serverShutdownBoundary;
public static VolmitSender getSender() {
@@ -637,6 +642,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
new BukkitLanguageSwitcher.Options("iris", "iris.all",
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
updateNotifier = new IrisUpdateNotifier(this);
PaperLibBootstrap.install();
SimdSupport.install();
timings.mark("bootstrap");
@@ -825,7 +831,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void addShutdownHook() {
removeShutdownHook();
serverLifecycleThread = Thread.currentThread();
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
try {
@@ -864,7 +869,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
*/
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
.thenApply(result -> new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message()));
.thenApply(this::managedWorldLoadResult);
}
private ManagedWorldLoader.ManagedWorldLoad managedWorldLoadResult(BukkitWorldReconciler.LoadResult result) {
if (!result.succeeded() && result.failure() != null) {
reportError("Could not explicitly load Iris world " + result.worldKey() + ": " + result.message(), result.failure());
}
return new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message());
}
public PendingWorldReplacementManager pendingWorldReplacements() {
@@ -928,6 +940,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
ServerConfigurator.restartAtStartupBoundary(restartReason);
return;
}
papiLifecycleActive = true;
setupPapi();
reportLockedRuntime();
}
@@ -974,6 +988,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public void onDisable() {
preservePendingBlockDrops();
papiLifecycleActive = false;
if (updateNotifier != null) {
updateNotifier.close();
updateNotifier = null;
}
if (debugDump != null) {
debugDump.close();
debugDump = null;
@@ -1013,6 +1033,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Override
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
preservePendingBlockDrops();
papiLifecycleActive = false;
teardownPapi();
if (IrisToolbelt.isServerStopping()) {
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
@@ -1140,7 +1162,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (generator == null) {
continue;
}
IrisToolbelt.beginWorldMaintenance(world, reason, true);
IrisToolbelt.beginWorldMaintenance(world, reason);
if (!deferredShutdownGenerators.contains(generator)) {
deferredShutdownGenerators.add(generator);
}
@@ -1152,52 +1174,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (!postStopFinisherStarted.compareAndSet(false, true)) {
return;
}
Thread activeServerThread = serverLifecycleThread;
if (activeServerThread == null) {
Iris.warn("Iris could not start its post-stop runtime finisher because the server lifecycle thread is unavailable.");
return;
}
Thread finisher = new Thread(() -> {
if (!awaitServerThreadTermination(activeServerThread)) {
return;
}
finishDeferredRuntimeTeardown("post-server-stop", 30L);
}, "Iris-PostStop-Finisher");
Thread finisher = new Thread(() -> finishRuntimeAfterServerShutdown("post-server-stop"),
"Iris-PostStop-Finisher");
finisher.setDaemon(false);
finisher.start();
}
static boolean awaitServerThreadTermination(Thread serverThread) {
if (serverThread == null || serverThread == Thread.currentThread()) {
return false;
}
try {
serverThread.join();
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.reportError("Iris post-stop runtime finisher was interrupted.", e);
return false;
}
}
private void runShutdownHook() {
if (startupBoundaryRestart.get()) {
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
return;
}
finishRuntimeAfterServerShutdown("shutdown-hook");
}
private void finishRuntimeAfterServerShutdown(String reason) {
if (!awaitServerShutdownBoundary()) {
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
SHUTDOWN_ERRORS.println("[Iris] The server did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
return;
}
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
finishDeferredRuntimeTeardown(reason, 30L);
}
private boolean awaitServerShutdownBoundary() {
ServerShutdownBoundary boundary = serverShutdownBoundary;
if (boundary == null) {
return true;
return false;
}
try {
return boundary.await(
@@ -1261,6 +1263,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
&& preservation.hasActiveResources();
}
private void preservePendingBlockDrops() {
for (World world : Bukkit.getWorlds()) {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator != null && generator.getEngine() != null
&& generator.getEngine().getWorldManager() instanceof IrisWorldManager manager) {
manager.flushPendingBlockDropsAsync().whenComplete((ignored, failure) -> {
if (failure != null) {
Iris.reportError("Failed to preserve pending block loot before Iris teardown.", failure);
}
});
}
}
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>();
List<PlatformChunkGenerator> generators = new ArrayList<>();
@@ -1315,8 +1331,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
private void setupPapi() {
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
@EventHandler
public void onPlaceholderApiEnable(PluginEnableEvent event) {
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
setupPapi();
}
}
@EventHandler
public void onPlaceholderApiDisable(PluginDisableEvent event) {
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
teardownPapi();
}
}
private synchronized void setupPapi() {
if (!papiLifecycleActive || papiRegistration != null
|| !PlaceholderRegistration.isPlaceholderApiEnabled()) {
return;
}
@@ -1343,7 +1374,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
papiRegistration = registration;
}
private void teardownPapi() {
private synchronized void teardownPapi() {
IrisPapiListener listener = papiListener;
papiListener = null;
@@ -1455,12 +1486,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public int getIrisVersion() {
String input = getDescription().getVersion();
int hyphenIndex = input.indexOf('-');
if (hyphenIndex != -1) {
String result = input.substring(0, hyphenIndex);
result = result.replaceAll("\\.", "");
return Integer.parseInt(result);
String release = hyphenIndex < 0 ? input : input.substring(0, hyphenIndex);
if (!release.matches("[0-9]+(?:\\.[0-9]+)*")) {
return -1;
}
try {
return Integer.parseInt(release.replace(".", ""));
} catch (NumberFormatException invalid) {
return -1;
}
return -1;
}
public int getMCVersion() {
@@ -81,7 +81,6 @@ import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.util.Vector;
@@ -309,23 +308,33 @@ public class CommandStudio implements DirectorExecutor {
O<Integer> ta = new O<>();
ta.set(-1);
var sender = sender();
var player = player();
var engine = engine();
VolmitSender sender = sender();
Player player = player();
Engine engine = engine();
ta.set(J.sr(() ->
{
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
ta.set(J.sr(() -> {
if (!J.runEntity(player, () -> {
if (player.getOpenInventory().getTopInventory() != inv) {
J.csr(ta.get());
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
return;
}
if (!add) {
inv.clear();
}
Location location = player.getLocation();
try {
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE,
location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
} catch (RuntimeException | Error failure) {
J.csr(ta.get());
Iris.reportError(failure);
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE,
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
}
}, 0, () -> J.csr(ta.get()))) {
J.csr(ta.get());
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
return;
}
if (!add) {
inv.clear();
}
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
}, fast ? 5 : 35));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
import art.arcane.iris.Iris;
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.world.task.J;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
@@ -13,6 +14,9 @@ import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricPublisher;
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
@@ -30,7 +34,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
public class IrisIntegrationService implements IrisService, IntegrationSnapshotProvider {
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
@@ -41,6 +45,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
"handshake",
"heartbeat",
"metrics",
IntegrationSnapshotProvider.CAPABILITY,
"metric-groups",
"iris-engine-metrics",
"iris-world-metrics"
@@ -63,6 +68,9 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
);
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
private final IntegrationMetricPublisher snapshots = new IntegrationMetricPublisher(
IntegrationMetricSchema.irisKeys().size(), 30_000L);
private int snapshotTaskId = -1;
public IrisIntegrationService() {
this(IrisIntegrationService::currentTelemetry);
@@ -77,11 +85,17 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
@Override
public void onEnable() {
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
snapshotTaskId = J.ar(this::publishSnapshots, 20);
Iris.verbose("Integration provider registered for Iris");
}
@Override
public void onDisable() {
if (snapshotTaskId != -1) {
J.car(snapshotTaskId);
snapshotTaskId = -1;
}
snapshots.clear();
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
}
@@ -188,6 +202,20 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
return Map.copyOf(selected);
}
@Override
public IntegrationMetricSnapshot snapshotMetrics(Set<String> metricKeys) {
return snapshots.snapshotMetrics(metricKeys, System.currentTimeMillis());
}
void publishSnapshots() {
IntegrationMetricPublisher.Demand demand = snapshots.demandedKeys(System.currentTimeMillis());
if (demand.keys().isEmpty()) {
return;
}
Map<String, IntegrationMetricSample> samples = sampleMetrics(demand.keys());
snapshots.publish(demand, System.currentTimeMillis(), samples);
}
@Override
public List<IntegrationMetricGroup> metricGroups() {
IrisTelemetrySnapshot snapshot = safeSnapshot();
@@ -0,0 +1,37 @@
package art.arcane.iris.update;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.volmlib.util.update.BukkitUpdateService;
import org.bukkit.plugin.Plugin;
import java.util.function.Consumer;
public final class IrisUpdateNotifier implements AutoCloseable {
private final Consumer<IrisSettings> settingsListener = this::reconfigure;
private final BukkitUpdateService updates;
private volatile boolean enabled;
private boolean closed;
public IrisUpdateNotifier(Plugin plugin) {
enabled = IrisSettings.get().getGeneral().isUpdateNotifications();
updates = BukkitUpdateService.register(plugin, new BukkitUpdateService.Options(
"VolmitSoftware", "Iris", "iris.update", () -> enabled));
IrisSettings.addSettingsListener(settingsListener);
reconfigure(IrisSettings.get());
}
private synchronized void reconfigure(IrisSettings settings) {
if (closed) {
return;
}
enabled = settings.getGeneral().isUpdateNotifications();
updates.reconfigure();
}
@Override
public synchronized void close() {
closed = true;
IrisSettings.removeSettingsListener(settingsListener);
updates.close();
}
}
@@ -896,14 +896,10 @@ public final class IrisWorldGeneratorResolver {
}
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
return IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
data.getDataFolder().getName(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
return dimension.dimensionTypeResourceKey(
IrisPlatforms.get().registries().generationRegistry(),
data.getDataFolder().getName()
);
}
private record LoadedHistory(GenerationHistory history, File activePack) {
@@ -19,6 +19,10 @@
package art.arcane.iris.world.runtime;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.world.event.IrisEngineHotloadEvent;
@@ -51,6 +55,13 @@ import org.bukkit.World;
import org.bukkit.WorldBorder;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
return service != null && service.removeGenerated(
BukkitWorldBinding.world(engine.getWorld()).getBlockAt(position.x(), position.y(), position.z()), entry);
}
@Override
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.inventory.ItemStack;
import java.util.Collection;
import java.util.Objects;
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
private final TreeFellerSVC service;
private final FellingRun run;
private final ItemStack tool;
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
super(block, player);
this.run = run;
this.service = service;
RoutedBlockBreakEvent(Options options) {
super(options.block(), options.player());
run = options.run();
service = options.service();
tool = Objects.requireNonNull(options.tool(), "tool").clone();
}
@Override
public Collection<ItemStack> captureNativeDrops() {
return getBlock().getDrops(tool);
}
@Override
public boolean routeDrop(Object drop) {
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
}
record Options(Block block, Player player, FellingRun run, TreeFellerSVC service, ItemStack tool) {
}
}
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
return;
}
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
BlockBreakEvent probe = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
block, run.candidate.player(), run, service, reservation.toolForDrops()));
service.managedEvents.add(probe);
try {
Bukkit.getPluginManager().callEvent(probe);
@@ -10,6 +10,9 @@ authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: VolmitSoftware.com
description: More than a Dimension!
permissions:
iris.update:
description: Allows viewing Iris updates in VolmLib's combined update reports.
default: op
iris.debugdump:
description: Allows creating and optionally uploading diagnostic reports.
default: op
@@ -25,34 +28,26 @@ permissions:
dependencies:
server:
PlaceholderAPI:
load: BEFORE
load: OMIT
required: false
join-classpath: true
CraftEngine:
load: BEFORE
load: OMIT
required: false
join-classpath: true
Nexo:
load: BEFORE
load: OMIT
required: false
join-classpath: true
Oraxen:
load: BEFORE
load: OMIT
required: false
join-classpath: true
ItemsAdder:
load: BEFORE
load: OMIT
required: false
join-classpath: true
SCore:
load: BEFORE
required: false
join-classpath: true
ExecutableItems:
load: BEFORE
required: false
join-classpath: false
MythicLib:
load: OMIT
required: false
join-classpath: true
@@ -61,11 +56,7 @@ dependencies:
required: false
join-classpath: true
eco:
load: BEFORE
required: false
join-classpath: true
EcoItems:
load: BEFORE
load: OMIT
required: false
join-classpath: true
MythicMobs:
@@ -77,7 +68,7 @@ dependencies:
required: false
join-classpath: true
KGenerators:
load: BEFORE
load: OMIT
required: false
join-classpath: true
Multiverse-Core:
@@ -85,6 +76,6 @@ dependencies:
required: false
join-classpath: true
WorldEdit:
load: BEFORE
load: OMIT
required: false
join-classpath: true
@@ -3,18 +3,6 @@ version: ${version}
main: ${main}
folia-supported: true
load: STARTUP
softdepend:
- PlaceholderAPI
- CraftEngine
- Nexo
- Oraxen
- ItemsAdder
- SCore
- ExecutableItems
- eco
- EcoItems
- KGenerators
- WorldEdit
loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: VolmitSoftware.com
@@ -23,6 +11,9 @@ commands:
iris:
aliases: [ ir, irs ]
permissions:
iris.update:
description: Allows viewing Iris updates in VolmLib's combined update reports.
default: op
iris.debugdump:
description: Allows creating and optionally uploading diagnostic reports.
default: op
@@ -0,0 +1,46 @@
{
"name": "Flat",
"derivative": "minecraft:snowy_plains",
"vanillaDerivative": "minecraft:snowy_plains",
"layers": [
{
"minHeight": 1,
"maxHeight": 1,
"palette": [
{
"block": "minecraft:lime_wool"
}
]
},
{
"minHeight": 384,
"maxHeight": 384,
"palette": [
{
"block": "minecraft:green_concrete"
}
]
}
],
"generators": [
{
"generator": "flat",
"min": 96,
"max": 96
}
],
"decorators": [
{
"palette": [
{
"block": "craftengine:default/flower_basket",
"data": {
"variant": "ground"
}
}
],
"chance": 1,
"forcePlace": true
}
]
}
@@ -0,0 +1,39 @@
{
"name": "CraftEngine Imported Feature Fixture",
"version": 1,
"mode": {
"type": "OVERWORLD"
},
"regions": [
"flat"
],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {
"min": -64,
"max": 320
},
"logicalHeight": 384,
"fluidHeight": 0,
"bedrock": false,
"rockPalette": {
"palette": [
{
"block": "minecraft:red_concrete"
}
]
},
"importedStructures": {
"disabled": [
"minecraft:"
]
},
"importedFeatures": {
"enabled": true,
"steps": [
"TOP_LAYER_MODIFICATION"
]
}
}
@@ -0,0 +1,15 @@
{
"interpolator": {
"function": "NONE",
"horizontalScale": 1
},
"seed": 310,
"composite": [
{
"seed": 310,
"style": {
"style": "FLAT"
}
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Flat",
"landBiomes": [
"flat"
],
"seaBiomes": [
"flat"
],
"shoreBiomes": [
"flat"
]
}
@@ -0,0 +1,35 @@
plugins {
id 'java'
}
version = '1.0.0'
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
}
dependencies {
compileOnly files(providers.gradleProperty('irisJar').get())
compileOnly files(providers.gradleProperty('craftEngineJar').get())
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
sourceSets {
main {
java.srcDirs = ['src']
resources.srcDirs = ['resources']
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += '-parameters'
}
tasks.named('jar') {
archiveBaseName = 'IrisCraftEngineNativeFixture'
}
@@ -0,0 +1,12 @@
name: IrisCraftEngineNativeFixture
version: 1.0.0
main: art.arcane.iris.fixture.CraftEngineNativeFixture
api-version: '26.1.2'
folia-supported: true
depend: [Iris, CraftEngine]
commands:
iriscenative:
permission: iriscenative.run
permissions:
iriscenative.run:
default: op
@@ -0,0 +1 @@
rootProject.name = 'iris-craftengine-native-fixture'
@@ -0,0 +1,385 @@
package art.arcane.iris.fixture;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.world.IrisCreator;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public final class CraftEngineNativeFixture extends JavaPlugin {
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "ce_native_probe");
private static final NamespacedKey FEATURE_WORLD_KEY = new NamespacedKey("iris", "ce_native_feature_probe");
private static final int TEST_Y = 180;
private static final int TEST_Z = 8;
private static final String BLOCK_ID = "craftengine:default/topaz_ore";
private static final String FURNITURE_ID = "craftengine:default/flower_basket[variant=ground]";
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (!(sender instanceof Player player) || arguments.length != 1) {
sender.sendMessage("CE_FAILED Use /iriscenative prepare|pending|neighbor|loaded|features|restart from a player");
return true;
}
if (arguments[0].equals("prepare")) {
player.setGameMode(GameMode.CREATIVE);
prepare(player);
return true;
}
if (arguments[0].equals("features")) {
importedFeatures(player);
return true;
}
World world = Bukkit.getWorld(WORLD_KEY);
if (world == null) {
fail(player, new IllegalStateException("Fixture world has not been created"));
return true;
}
if (arguments[0].equals("restart")) {
loadChunks().thenCompose(ignored -> J.runRegionFuture(world, 0, 0,
() -> later(player, world, () -> restarted(player, world))))
.whenComplete((ignored, failure) -> {
if (failure != null) {
fail(player, failure);
}
});
return true;
}
J.runRegionFuture(world, 0, 0, () -> {
switch (arguments[0]) {
case "pending" -> pending(player, world);
case "neighbor" -> neighbor(player, world);
case "loaded" -> invalidateLoaded(player, world, IrisToolbelt.access(world).getEngine());
default -> throw new IllegalArgumentException("Unknown fixture operation");
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
fail(player, failure);
}
});
return true;
}
private void prepare(Player player) {
CompletableFuture.runAsync(() -> {
if (Bukkit.getWorld(WORLD_KEY) == null) {
try {
new IrisCreator().name(WORLD_KEY.toString()).dimension("ce-native-fixture")
.seed(78264193L).create();
} catch (Exception failure) {
throw new IllegalStateException("Could not create CraftEngine fixture world", failure);
}
}
}).thenCompose(ignored -> loadChunks()).thenCompose(ignored -> {
World world = Bukkit.getWorld(WORLD_KEY);
return J.runRegionFuture(world, 0, 0, () -> preparePlatform(world));
}).thenAccept(ignored -> {
World world = Bukkit.getWorld(WORLD_KEY);
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, TEST_Y + 3, 8.5))
.whenComplete((teleported, failure) -> {
if (failure != null || !Boolean.TRUE.equals(teleported)) {
fail(player, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
return;
}
player.sendMessage("CE_READY world=" + WORLD_KEY + " folia=" + J.isFolia());
}))) {
throw new IllegalStateException("Could not schedule fixture teleport");
}
}).exceptionally(failure -> {
fail(player, failure);
return null;
});
}
private void importedFeatures(Player player) {
CompletableFuture.runAsync(() -> {
if (Bukkit.getWorld(FEATURE_WORLD_KEY) == null) {
try {
new IrisCreator().name(FEATURE_WORLD_KEY.toString()).dimension("ce-native-feature-fixture")
.seed(78264193L).create();
} catch (Exception failure) {
throw new IllegalStateException("Could not create imported feature fixture world", failure);
}
}
}).thenCompose(ignored -> {
World world = Bukkit.getWorld(FEATURE_WORLD_KEY);
return world.getChunkAtAsync(0, 0, true);
}).thenCompose(chunk -> J.runRegionFuture(chunk.getWorld(), 0, 0, () -> {
World world = chunk.getWorld();
Engine engine = IrisToolbelt.access(world).getEngine();
int snow = 0;
int pending = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (world.getBlockAt(x, 97, z).getType() == Material.SNOW) {
snow++;
}
if (engine.getMantle().getMantle().get(x, 97 - engine.getMinHeight(), z, Identifier.class) != null) {
pending++;
}
}
}
require(snow == 256, "Imported snow feature did not cover all 256 test columns: " + snow);
require(pending == 0, "Imported feature left pending furniture records: " + pending);
for (Entity entity : chunk.getEntities()) {
require(!CraftEngineFurniture.isFurniture(entity), "Imported snow feature left generated furniture");
}
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, 101, 8.5))
.whenComplete((teleported, failure) -> {
if (failure != null || !Boolean.TRUE.equals(teleported)) {
fail(player, failure == null ? new IllegalStateException("Feature world teleport failed") : failure);
return;
}
player.sendMessage("CE_FEATURES snow=256 pending=0 furniture=0");
}))) {
throw new IllegalStateException("Could not schedule feature world teleport");
}
})).exceptionally(failure -> {
fail(player, failure);
return null;
});
}
private CompletableFuture<Void> loadChunks() {
World world = Bukkit.getWorld(WORLD_KEY);
require(world != null && IrisToolbelt.isIrisWorld(world), "Fixture must use a real Iris world");
List<CompletableFuture<Void>> chunks = new ArrayList<>(15);
for (int x = -1; x <= 3; x++) {
for (int z = -1; z <= 1; z++) {
chunks.add(world.getChunkAtAsync(x, z, true).thenCompose(chunk ->
J.runRegionFuture(world, chunk.getX(), chunk.getZ(), () -> chunk.addPluginChunkTicket(this))));
}
}
return CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new));
}
private void preparePlatform(World world) {
require(CraftEngineBlocks.byId(Key.of("default:topaz_ore")) != null, "CraftEngine default block is missing");
require(CraftEngineFurniture.byId(Key.of("default:flower_basket")) != null, "CraftEngine furniture is missing");
for (int chunkX = 0; chunkX <= 2; chunkX++) {
for (Entity entity : world.getChunkAt(chunkX, 0).getEntities()) {
if (CraftEngineFurniture.isFurniture(entity)) {
CraftEngineFurniture.remove(entity, false, false);
}
}
}
for (int x = 0; x <= 47; x++) {
for (int z = 2; z <= 14; z++) {
world.getBlockAt(x, TEST_Y - 1, z).setType(Material.STONE, false);
for (int y = TEST_Y; y <= TEST_Y + 5; y++) {
world.getBlockAt(x, y, z).setType(Material.AIR, false);
}
}
}
}
private void pending(Player player, World world) {
Engine engine = IrisToolbelt.access(world).getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
queue(engine, world, 2, BLOCK_ID);
queue(engine, world, 4, BLOCK_ID);
queue(engine, world, 6, BLOCK_ID);
queue(engine, world, 8, BLOCK_ID);
queue(engine, world, 10, FURNITURE_ID);
queue(engine, world, 12, FURNITURE_ID);
Block chest = world.getBlockAt(14, TEST_Y, TEST_Z);
chest.setType(Material.CHEST, false);
mantle.set(14, TEST_Y - engine.getMinHeight(), TEST_Z, new TileWrapper(TileData.getTileState(chest, false)));
world.getBlockAt(4, TEST_Y, TEST_Z).setType(Material.AIR, false);
world.getBlockAt(6, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
changes.add(8, TEST_Y, TEST_Z);
changes.add(12, TEST_Y - 1, TEST_Z);
changes.add(12, TEST_Y, TEST_Z);
changes.add(14, TEST_Y, TEST_Z);
world.getBlockAt(8, TEST_Y, TEST_Z).setType(Material.AIR, false);
world.getBlockAt(12, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
chest.setType(Material.COBBLESTONE, false);
DeferredPlacementRecords.invalidate(engine, changes);
require(record(engine, 8, Identifier.class) == null, "Native cut retained a pending custom record");
require(record(engine, 12, Identifier.class) == null, "Flatten column retained pending furniture");
require(record(engine, 14, TileWrapper.class) == null, "Native wall retained pending chest data");
require(record(engine, 2, Identifier.class) != null, "Native writes invalidated an untouched custom record");
replay(engine, world, 0);
CustomBlockOwnership ownership = record(engine, 10, CustomBlockOwnership.class);
require(record(engine, 2, Identifier.class) == null, "Custom materialization did not consume pending records");
require(CraftEngineBlocks.isCustomBlock(world.getBlockAt(2, TEST_Y, TEST_Z)), "Intact custom block was not placed");
require(world.getBlockAt(4, TEST_Y, TEST_Z).getType().isAir(), "Cut custom block was resurrected");
require(world.getBlockAt(6, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE, "Custom block replaced native wall");
require(world.getBlockAt(8, TEST_Y, TEST_Z).getType().isAir(), "Invalidated custom block was resurrected");
require(chest.getType() == Material.COBBLESTONE, "Chest data replaced native wall");
later(player, world, () -> {
int intactFurniture = furnitureAt(world, 10);
require(intactFurniture == 1, "Intact furniture count is " + intactFurniture
+ "; pending=" + record(engine, 10, Identifier.class)
+ "; ownership=" + ownership
+ "; chunkEntities=" + world.getChunkAt(0, 0).getEntities().length);
require(furnitureAt(world, 12) == 0, "Flattened furniture was resurrected");
player.sendMessage("CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1");
});
}
private void neighbor(Player player, World world) {
Engine engine = IrisToolbelt.access(world).getEngine();
queue(engine, world, 18, FURNITURE_ID);
queue(engine, world, 20, FURNITURE_ID);
queue(engine, world, 22, FURNITURE_ID);
queue(engine, world, 42, FURNITURE_ID);
replay(engine, world, 1);
replay(engine, world, 2);
world.getBlockAt(40, TEST_Y, TEST_Z).setType(Material.DIAMOND_BLOCK, false);
later(player, world, () -> {
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
require(furnitureAt(world, 42) == 1 && record(engine, 42, CustomBlockOwnership.class) != null,
"Regeneration target furniture did not materialize with saved ownership");
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 40.5, TEST_Y + 3, TEST_Z + 0.5))
.whenComplete((teleported, failure) -> {
if (failure != null || !Boolean.TRUE.equals(teleported)) {
fail(player, failure == null ? new IllegalStateException("Regeneration target teleport failed") : failure);
return;
}
player.sendMessage("CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1");
}))) {
throw new IllegalStateException("Could not schedule regeneration target teleport");
}
});
}
private void invalidateLoaded(Player player, World world, Engine engine) {
require(world.getBlockAt(40, TEST_Y, TEST_Z).getType().isAir(), "Adjacent target chunk did not regenerate");
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
changes.add(18, TEST_Y - 1, TEST_Z);
changes.add(18, TEST_Y, TEST_Z);
changes.add(22, TEST_Y, TEST_Z);
world.getBlockAt(18, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
world.getBlockAt(18, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
DeferredPlacementRecords.invalidate(engine, changes);
require(CraftEngineFurniture.place(new Location(world, 22.5, TEST_Y, TEST_Z + 0.5),
Key.of("default:flower_basket"), "ground", false) != null,
"Could not place replacement furniture before scheduled cleanup");
later(player, world, () -> {
require(furnitureAt(world, 42) == 0, "Regeneration retained target chunk furniture");
require(furnitureAt(world, 18) == 0, "Native write left materialized neighbor furniture");
require(furnitureAt(world, 20) == 1, "Native write removed untouched neighbor furniture");
require(furnitureAt(world, 22) == 1, "Native cleanup removed replacement furniture or retained the old one");
require(world.getBlockAt(18, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE,
"Furniture cleanup replaced the native wall");
player.sendMessage("CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1");
});
}
private void restarted(Player player, World world) {
require(furnitureAt(world, 20) == 1, "Generated neighbor furniture did not survive restart");
require(furnitureAt(world, 10) == 1, "Untouched furniture did not survive restart");
Engine engine = IrisToolbelt.access(world).getEngine();
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
changes.add(20, TEST_Y, TEST_Z);
DeferredPlacementRecords.invalidate(engine, changes);
later(player, world, () -> {
require(furnitureAt(world, 20) == 0, "Persisted furniture ownership was not invalidated after restart");
require(furnitureAt(world, 10) == 1, "Restart cleanup removed untouched furniture");
player.sendMessage("CE_RESTART removed=1 untouched=1");
});
}
private void queue(Engine engine, World world, int x, String identifier) {
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
BlockData data = service.getBlockData(Identifier.fromString(identifier)).orElseThrow();
require(data instanceof IrisCustomData, "Iris did not resolve real CraftEngine content: " + identifier);
IrisCustomData custom = (IrisCustomData) data;
world.getBlockAt(x, TEST_Y, TEST_Z).setBlockData(custom.getBase(), false);
engine.getMantle().getMantle().set(x, TEST_Y - engine.getMinHeight(), TEST_Z, custom.getCustom());
}
private <T> T record(Engine engine, int x, Class<T> type) {
return engine.getMantle().getMantle().get(x, TEST_Y - engine.getMinHeight(), TEST_Z, type);
}
private void replay(Engine engine, World world, int chunkX) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
for (int z = -1; z <= 1; z++) {
require(world.isChunkLoaded(x, z), "Replay neighbor is not loaded: " + x + "," + z);
}
}
require(mantle.isLoaded(world.getChunkAt(chunkX, 0)), "Replay mantle chunk is not loaded: " + chunkX);
mantle.flag(chunkX, 0, MantleFlag.ETCHED, false);
mantle.flag(chunkX, 0, MantleFlag.TILE, false);
mantle.flag(chunkX, 0, MantleFlag.CUSTOM, false);
EngineBukkitOps.updateChunk(engine, world.getChunkAt(chunkX, 0), mantle);
}
private int furnitureAt(World world, int x) {
int count = 0;
for (Entity entity : world.getChunkAt(x >> 4, TEST_Z >> 4).getEntities()) {
Location location = entity.getLocation();
if (location.getBlockX() == x && location.getBlockY() == TEST_Y && location.getBlockZ() == TEST_Z
&& CraftEngineFurniture.getLoadedFurnitureByMetaEntity(entity) != null) {
count++;
}
}
return count;
}
private void later(Player player, World world, Runnable assertion) {
Bukkit.getRegionScheduler().runDelayed(this, new Location(world, 8, TEST_Y, TEST_Z), task -> {
try {
assertion.run();
} catch (Throwable failure) {
fail(player, failure);
}
}, 20);
}
private NativeBlockChangeSet.Bounds bounds(World world) {
return new NativeBlockChangeSet.Bounds(-1, -1, 3, 1, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
}
private void require(boolean condition, String message) {
if (!condition) {
throw new IllegalStateException(message);
}
}
private void fail(CommandSender sender, Throwable failure) {
getLogger().log(Level.SEVERE, "CraftEngine native placement assertion failed", failure);
sender.sendMessage("CE_FAILED " + failure.getMessage());
}
}
@@ -0,0 +1,32 @@
{
"name": "Flat",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{
"minHeight": 1,
"maxHeight": 1,
"palette": [
{
"block": "minecraft:lime_wool"
}
]
},
{
"minHeight": 384,
"maxHeight": 384,
"palette": [
{
"block": "minecraft:green_concrete"
}
]
}
],
"generators": [
{
"generator": "flat",
"min": 96,
"max": 96
}
]
}
@@ -0,0 +1,33 @@
{
"name": "CraftEngine Native Fixture",
"version": 1,
"mode": {
"type": "OVERWORLD"
},
"regions": [
"flat"
],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {
"min": -64,
"max": 320
},
"logicalHeight": 384,
"fluidHeight": 0,
"bedrock": false,
"rockPalette": {
"palette": [
{
"block": "minecraft:red_concrete"
}
]
},
"importedStructures": {
"disabled": [
"minecraft:"
]
}
}
@@ -0,0 +1,15 @@
{
"interpolator": {
"function": "NONE",
"horizontalScale": 1
},
"seed": 310,
"composite": [
{
"seed": 310,
"style": {
"style": "FLAT"
}
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Flat",
"landBiomes": [
"flat"
],
"seaBiomes": [
"flat"
],
"shoreBiomes": [
"flat"
]
}
@@ -0,0 +1,15 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
export default {
name: 'iris-craftengine-native-restart',
description: 'Verify persisted generated furniture ownership after a full server restart.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
await context.step('native writes remove persisted ownership after restart', async () => {
const response = await context.command('/iriscenative restart', /CE_RESTART|CE_FAILED/, 60000)
context.expect(response.includes('CE_RESTART removed=1 untouched=1'), response)
})
}
}
@@ -0,0 +1,49 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
export default {
name: 'iris-craftengine-native',
description: 'Verify deferred native invalidation and loaded neighbor cleanup with real Iris and CraftEngine.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
await context.step('create and enter a real Iris world with CraftEngine content', async () => {
const response = await context.command('/iriscenative prepare', /CE_READY|CE_FAILED/, 180000)
context.expect(response.includes('CE_READY'), response)
})
await context.step('native writes remove pending block, chest, and flatten furniture records', async () => {
const response = await context.command('/iriscenative pending', /CE_PENDING|CE_FAILED/, 60000)
context.expect(response.includes('CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1'), response)
await context.waitUntil(() => {
const origin = context.bot.entity.position.floored()
const cut = context.bot.blockAt(origin.set(4, 180, 8))
const wall = context.bot.blockAt(origin.set(6, 180, 8))
const invalidated = context.bot.blockAt(origin.set(8, 180, 8))
const chest = context.bot.blockAt(origin.set(14, 180, 8))
return cut?.name === 'air' && wall?.name === 'cobblestone'
&& invalidated?.name === 'air' && chest?.name === 'cobblestone'
}, { label: 'Client observes retained native air and walls', timeoutMs: 30000, intervalMs: 100 })
})
await context.step('regeneration removes target furniture and preserves neighbor ownership', async () => {
const ready = await context.command('/iriscenative neighbor', /CE_NEIGHBOR_READY|CE_FAILED/, 30000)
context.expect(ready.includes('CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1'), ready)
const regenerated = await context.command('/iris developer regen radius=0', /Regen complete:|Regen failed:/, 90000)
context.expect(regenerated.includes('Regen complete:'), regenerated)
const response = await context.command('/iriscenative loaded', /CE_LOADED|CE_FAILED/, 30000)
context.expect(response.includes('CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1'), response)
})
await context.step('imported snow features replace generated furniture records', async () => {
const response = await context.command('/iriscenative features', /CE_FEATURES|CE_FAILED/, 180000)
context.expect(response.includes('CE_FEATURES snow=256 pending=0 furniture=0'), response)
await context.waitUntil(() => {
const position = context.bot.entity.position.floored()
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
if (context.bot.blockAt(position.set(x, 97, z))?.name !== 'snow') return false
}
}
return true
}, { label: 'Client observes 256 imported snow layers', timeoutMs: 30000, intervalMs: 100 })
})
}
}
@@ -2,6 +2,7 @@ name: IrisNativeLifecycleFixture
version: 1.0.0
main: art.arcane.iris.fixture.NativeLifecycleFixture
api-version: '26.1.2'
folia-supported: true
depend: [Iris]
commands:
irislifecycle:
@@ -1,8 +1,12 @@
package art.arcane.iris.fixture;
import art.arcane.iris.Iris;
import art.arcane.volmlib.nativelib.NativeAdapters;
import art.arcane.iris.world.runtime.RuntimeInjection;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.world.IrisCreator;
import art.arcane.iris.world.task.J;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.IrisWorldStorage;
import art.arcane.iris.world.WorldCreatorCompat;
@@ -13,6 +17,15 @@ import org.bukkit.entity.Player;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.Container;
import org.bukkit.loot.Lootable;
import org.bukkit.loot.LootTable;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapView;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
@@ -38,12 +51,15 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
private static final NamespacedKey TREASURE_KEY = new NamespacedKey("iris", "treasure_map_probe");
private static final long WORLD_SEED = 78264193L;
private NativeWorldRuntime runtime;
private TreasureLootChecks treasureLootChecks;
@Override
public void onEnable() {
runtime = NativeAdapters.require(NativeWorldRuntime.class);
treasureLootChecks = new TreasureLootChecks(this);
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
getLogger().info("Native lifecycle ready: " + runtime.description()
+ "; terrain provider=" + terrain.getClass().getName());
@@ -56,12 +72,32 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (arguments.length != 1) {
if (arguments.length == 0) {
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
return true;
}
try {
switch (arguments[0]) {
case "treasure-map-create" -> createTreasureMapWorld(sender);
case "treasure-map-limit" -> runTreasureRegion(sender, () -> checkTreasureMapLimit(sender));
case "treasure-map-chest" -> {
if (arguments.length != 3) {
throw new IllegalArgumentException("Use treasure-map-chest <destination> <CHEST|COPPER_CHEST>");
}
runTreasureRegion(sender, () -> prepareTreasureMapChest(sender, arguments[1], arguments[2]));
}
case "treasure-map-inspect" -> {
if (arguments.length != 2) {
throw new IllegalArgumentException("Use treasure-map-inspect <destination>");
}
runTreasureRegion(sender, () -> inspectTreasureMapChest(sender, arguments[1]));
}
case "treasure-map-check" -> runTreasureRegion(sender, () -> verifyTreasureMapLoot(sender));
case "treasure-extra-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verify(sender));
case "treasure-region-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyRegion(sender));
case "treasure-reload-before" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, false));
case "treasure-reload-after" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, true));
case "treasure-resources" -> treasureLootChecks.reportResourceReloads(sender);
case "create" -> create(sender);
case "unload" -> unload(sender);
case "structures" -> structures(sender);
@@ -80,6 +116,177 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
return true;
}
private void createTreasureMapWorld(CommandSender sender) {
if (!(sender instanceof Player player)) {
throw new IllegalStateException("Treasure map fixture requires a player");
}
runTreasureRegion(sender, () -> prepareTreasureMapWorld(player));
}
private void runTreasureRegion(CommandSender sender, Runnable task) {
ensureTreasureMapWorld()
.thenCompose(ignored -> J.runRegionFuture(Bukkit.getWorld(TREASURE_KEY), 0, 0, task))
.whenComplete((ignored, failure) -> {
if (failure != null) {
fail(sender, failure);
}
});
}
private CompletableFuture<?> ensureTreasureMapWorld() {
if (Bukkit.getWorld(TREASURE_KEY) != null) {
return CompletableFuture.completedFuture(null);
}
if (WorldCreatorCompat.persistentLevelRoot(TREASURE_KEY).exists()) {
return Iris.instance.worldReconciler().loadWorld(ServerProperties.BUKKIT_YML, TREASURE_KEY.toString());
}
return runAsync(() -> {
try {
new IrisCreator().name(TREASURE_KEY.toString()).dimension("treasure-map-fixture")
.seed(WORLD_SEED).create();
} catch (Exception failure) {
throw new IllegalStateException("Treasure map fixture creation failed", failure);
}
});
}
private void prepareTreasureMapWorld(Player player) {
World world = Bukkit.getWorld(TREASURE_KEY);
if (world == null || !IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED
|| !"treasure-map-fixture".equals(IrisToolbelt.access(world).getEngine().getDimension().getLoadKey())) {
throw new IllegalStateException("Treasure map fixture world identity is invalid");
}
for (int x = 0; x <= 8; x++) {
for (int z = 0; z <= 8; z++) {
world.getBlockAt(x, 160, z).setType(Material.STONE, false);
for (int y = 161; y <= 164; y++) {
world.getBlockAt(x, y, z).setType(Material.AIR, false);
}
}
}
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 0.5, 161, 0.5)).whenComplete((teleported, failure) -> {
if (failure != null) {
fail(player, failure);
} else if (!Boolean.TRUE.equals(teleported)) {
fail(player, new IllegalStateException("Treasure map fixture teleport failed"));
} else {
player.sendMessage("TREASURE_MAP_READY world=" + TREASURE_KEY
+ " version=" + Bukkit.getMinecraftVersion() + " folia=" + J.isFolia());
}
}))) {
throw new IllegalStateException("Could not schedule treasure map fixture teleport");
}
}
private void checkTreasureMapLimit(CommandSender sender) {
World world = Bukkit.getWorld(TREASURE_KEY);
if (world == null) {
throw new IllegalStateException("Treasure map fixture world is absent");
}
Engine engine = IrisToolbelt.access(world).getEngine();
IrisStructureLocator.LocateResult located = IrisStructureLocator.locateInPlacementRings(
engine, "minecraft:buried_treasure", 2, 2, 50, (x, z) -> true);
if (located.status() != IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Blocked treasure placement returned " + located.status());
}
sender.sendMessage("TREASURE_MAP_LIMIT status=" + located.status());
}
private void prepareTreasureMapChest(CommandSender sender, String destination, String material) {
World world = Bukkit.getWorld(TREASURE_KEY);
treasureMapChest(world, destination, material);
sender.sendMessage("TREASURE_MAP_CHEST destination=" + destination + " material=" + material);
}
private Container treasureMapChest(World world, String destination, String material) {
requireTreasureDestination(destination);
if (!List.of("CHEST", "COPPER_CHEST").contains(material)) {
throw new IllegalArgumentException("Unknown treasure map container: " + material);
}
LootTable loot = Bukkit.getLootTable(new NamespacedKey("iris_qa", destination));
if (loot == null) {
throw new IllegalStateException("Treasure map loot table is absent: " + destination);
}
world.getBlockAt(2, 161, 2).setType(Material.AIR, false);
world.getBlockAt(2, 161, 2).setType(Material.valueOf(material), false);
Container chest = (Container) world.getBlockAt(2, 161, 2).getState();
((Lootable) chest).setLootTable(loot);
((Lootable) chest).setSeed(WORLD_SEED);
if (!chest.update(true, false)) {
throw new IllegalStateException("Treasure map loot chest was not installed");
}
return (Container) world.getBlockAt(2, 161, 2).getState();
}
private void verifyTreasureMapLoot(CommandSender sender) {
checkTreasureMapLimit(sender);
World world = Bukkit.getWorld(TREASURE_KEY);
for (String destination : List.of("blocked", "disabled", "found", "mixed")) {
Inventory inventory = treasureMapChest(world, destination, "CHEST").getInventory();
inspectTreasureMapChest(sender, destination);
inventory.remove(Material.DIAMOND);
Inventory reopened = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
if (reopened.contains(Material.DIAMOND)
|| !(reopened.contains(Material.FILLED_MAP) || reopened.contains(Material.MAP))) {
throw new IllegalStateException("Reopening changed " + destination + " chest loot");
}
}
sender.sendMessage("TREASURE_MAP_CHECK destinations=4 retained=true");
}
private void inspectTreasureMapChest(CommandSender sender, String destination) {
requireTreasureDestination(destination);
World world = Bukkit.getWorld(TREASURE_KEY);
Inventory inventory = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
ItemStack map = null;
int diamonds = 0;
for (ItemStack item : inventory.getContents()) {
if (item == null) {
continue;
}
if ((item.getType() == Material.MAP || item.getType() == Material.FILLED_MAP)
&& item.getAmount() == 1 && map == null) {
map = item;
} else if (item.getType() == Material.DIAMOND) {
diamonds += item.getAmount();
} else {
throw new IllegalStateException("Unexpected chest loot: " + item);
}
}
if (map == null || diamonds != 1) {
throw new IllegalStateException("Invalid " + destination + " chest: map=" + map + " diamonds=" + diamonds);
}
boolean expectedLocated = destination.equals("found") || destination.equals("mixed");
MapView view = map.getItemMeta() instanceof MapMeta meta && meta.hasMapView() ? meta.getMapView() : null;
if ((view != null) != expectedLocated) {
throw new IllegalStateException("Unexpected map data for " + destination + ": " + map);
}
String result = "TREASURE_MAP_INSPECT destination=" + destination + " diamonds=1 located=" + expectedLocated;
if (view != null) {
StructureSearchResult target = world.locateNearestStructure(
new Location(world, 2, 161, 2), Structure.DESERT_PYRAMID, 50, false);
if (target == null || target.getStructure() != Structure.DESERT_PYRAMID) {
throw new IllegalStateException("Map has no verified pyramid target");
}
int targetX = target.getLocation().getBlockX();
int targetZ = target.getLocation().getBlockZ();
if (view.getWorld() != world || view.getScale() != MapView.Scale.CLOSEST
|| view.getCenterX() != Math.floorDiv(targetX + 64, 128) * 128
|| view.getCenterZ() != Math.floorDiv(targetZ + 64, 128) * 128) {
throw new IllegalStateException("Map data does not cover the verified pyramid at " + target.getLocation());
}
result += " mapId=" + view.getId() + " targetX=" + targetX + " targetZ=" + targetZ
+ " centerX=" + view.getCenterX() + " centerZ=" + view.getCenterZ();
}
sender.sendMessage(result);
}
private void requireTreasureDestination(String destination) {
if (!List.of("blocked", "disabled", "found", "mixed").contains(destination)) {
throw new IllegalArgumentException("Unknown treasure map destination: " + destination);
}
}
private void lifecycleStatus(CommandSender sender) throws Exception {
World world = Bukkit.getWorld(COMMAND_KEY);
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
@@ -282,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
}
public boolean regionized() {
return false;
return J.isFolia();
}
public boolean primaryThread() {
return Bukkit.isPrimaryThread();
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
}
public CompletableFuture<Void> runGlobal(Runnable task) {
@@ -299,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
result.completeExceptionally(failure);
}
};
if (Bukkit.isPrimaryThread()) {
if (primaryThread()) {
wrapped.run();
} else {
Bukkit.getScheduler().runTask(this, wrapped);
} else if (!J.runGlobal(wrapped)) {
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
}
return result;
}
@@ -0,0 +1,259 @@
package art.arcane.iris.fixture;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.loot.InventorySlotType;
import art.arcane.iris.world.loot.IrisVanillaLootTable;
import art.arcane.iris.world.loot.IrisLootTable;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.matter.Matter;
import io.papermc.paper.event.world.StructuresLocateEvent;
import io.papermc.paper.event.server.ServerResourcesReloadedEvent;
import io.papermc.paper.math.Position;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Container;
import org.bukkit.block.Chest;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.LootGenerateEvent;
import org.bukkit.generator.structure.Structure;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Inventory;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.StructureSearchResult;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public final class TreasureLootChecks implements Listener {
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "treasure_map_probe");
private static final List<String> CHESTS = List.of(
"CHEST", "COPPER_CHEST", "EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST",
"OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST",
"WAXED_WEATHERED_COPPER_CHEST", "WAXED_OXIDIZED_COPPER_CHEST");
private final IrisVanillaLootTable reloadTable = new IrisVanillaLootTable("iris_qa:reload");
private final AtomicInteger resourceReloads = new AtomicInteger();
private LocateAction locateAction = LocateAction.PASS;
private int locateEvents;
private int lootEvents;
private boolean lootEventsOwned = true;
public TreasureLootChecks(JavaPlugin plugin) {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void verify(CommandSender sender) {
World world = requireWorld();
verifyCopperLoot(world);
sender.sendMessage("TREASURE_COPPER_LOOT variants=8 normal=true diamonds=1");
verifyInventoryRollback(world);
sender.sendMessage("TREASURE_LOOT_ROLLBACK contents=true nativeTable=true seed=true retry=true");
verifyLocateEvents(world);
sender.sendMessage("TREASURE_LOCATE_EVENTS cancelled=true replaced=true calls=2");
sender.sendMessage("TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true");
}
public void verifyRegion(CommandSender sender) {
World world = requireWorld();
if (!J.isOwnedByCurrentRegion(world, 0, 0)) {
throw new IllegalStateException("Fixture does not own its loot region");
}
lootEvents = 0;
lootEventsOwned = true;
verifyCopperLoot(world);
if (lootEvents != CHESTS.size() || !lootEventsOwned) {
throw new IllegalStateException("Loot event ownership failed: count=" + lootEvents
+ " owned=" + lootEventsOwned);
}
sender.sendMessage("TREASURE_REGION_LOOT copper=8 normal=1 events=" + lootEvents
+ " owned=true folia=" + J.isFolia());
}
@EventHandler
public void onLoot(LootGenerateEvent event) {
if (event.getWorld().getKey().equals(WORLD_KEY) && event.isPlugin()) {
Location origin = event.getLootContext().getLocation();
lootEvents++;
lootEventsOwned &= J.isOwnedByCurrentRegion(event.getWorld(),
origin.getBlockX() >> 4, origin.getBlockZ() >> 4);
}
}
public void verifyReload(CommandSender sender, boolean reloaded) {
World world = requireWorld();
Material expected = reloaded ? Material.EMERALD : Material.DIAMOND;
Collection<ItemStack> items = reloadTable.getLoot(
false, 783L, InventorySlotType.STORAGE, world, 2, 161, 2);
if (items.size() != 1 || items.iterator().next().getType() != expected) {
throw new IllegalStateException("Reloaded loot did not contain " + expected + ": " + items);
}
sender.sendMessage("TREASURE_RELOAD material=" + expected);
}
public void reportResourceReloads(CommandSender sender) {
sender.sendMessage("TREASURE_RESOURCES revision=" + resourceReloads.get());
}
@EventHandler
public void onResourcesReloaded(ServerResourcesReloadedEvent event) {
resourceReloads.incrementAndGet();
}
@EventHandler
public void onLocate(StructuresLocateEvent event) {
if (!event.getWorld().getKey().equals(WORLD_KEY) || locateAction == LocateAction.PASS) {
return;
}
locateEvents++;
if (locateAction == LocateAction.CANCEL) {
event.setCancelled(true);
} else {
event.setResult(new StructuresLocateEvent.Result(Position.block(123, 100, 456), Structure.DESERT_PYRAMID));
}
}
private void verifyCopperLoot(World world) {
Engine engine = IrisToolbelt.access(world).getEngine();
Block block = world.getBlockAt(4, 161, 4);
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(block.getChunk());
try {
for (String name : CHESTS) {
block.setType(Material.AIR, false);
block.setType(Material.valueOf(name), false);
if (!BukkitBlockResolution.isStorageChest(block.getBlockData())) {
throw new IllegalStateException("Chest is excluded from Iris loot: " + name);
}
EngineBukkitOps.update(engine, 4, 161, 4, block.getChunk(), mantle);
Container chest = (Container) block.getState();
int diamonds = 0;
for (ItemStack item : chest.getInventory().getContents()) {
if (item == null) {
continue;
}
if (item.getType() != Material.DIAMOND) {
throw new IllegalStateException("Unexpected " + name + " loot: " + item);
}
diamonds += item.getAmount();
}
if (diamonds != 1) {
throw new IllegalStateException(name + " contains " + diamonds + " diamonds");
}
}
} finally {
mantle.release();
}
}
private void verifyInventoryRollback(World world) {
Engine engine = IrisToolbelt.access(world).getEngine();
Block block = world.getBlockAt(6, 161, 4);
block.setType(Material.AIR, false);
block.setType(Material.CHEST, false);
Chest chest = (Chest) block.getState();
chest.getBlockInventory().setItem(0, new ItemStack(Material.EMERALD, 3));
chest = (Chest) block.getState();
NamespacedKey key = new NamespacedKey("iris_qa", "reload");
chest.setLootTable(Bukkit.getLootTable(key));
chest.setSeed(913L);
if (!chest.update(true, false)) {
throw new IllegalStateException("Could not set native loot for rollback check");
}
Inventory inventory = chest.getBlockInventory();
IllegalStateException injected = new IllegalStateException("Fixture insertion interruption");
Inventory interrupted = (Inventory) Proxy.newProxyInstance(Inventory.class.getClassLoader(),
new Class<?>[]{Inventory.class}, (proxy, method, arguments) -> {
Object result;
try {
result = method.invoke(inventory, arguments);
} catch (InvocationTargetException failure) {
throw failure.getCause();
}
if (method.getName().equals("addItem")) {
throw injected;
}
return result;
});
KList<IrisLootTable> tables = new KList<>();
tables.add(new IrisVanillaLootTable("iris_qa:reload"));
try {
EngineBukkitOps.addItems(engine, false, interrupted, tables, InventorySlotType.STORAGE,
world, 6, 161, 4);
throw new IllegalStateException("Interrupted insertion did not fail");
} catch (IllegalStateException failure) {
if (failure != injected) {
throw failure;
}
}
Chest restored = (Chest) block.getState();
if (restored.getLootTable() == null || !restored.getLootTable().getKey().equals(key)
|| restored.getSeed() != 913L) {
throw new IllegalStateException("Rollback lost the native loot table or seed");
}
ItemStack existing = restored.getSnapshotInventory().getItem(0);
if (existing == null || existing.getType() != Material.EMERALD || existing.getAmount() != 3) {
throw new IllegalStateException("Rollback lost the original contents: " + existing);
}
EngineBukkitOps.addItems(engine, false, restored.getBlockInventory(), tables, InventorySlotType.STORAGE,
world, 6, 161, 4);
int diamonds = 0;
int emeralds = 0;
for (ItemStack item : restored.getBlockInventory().getContents()) {
if (item == null) {
continue;
}
if (item.getType() == Material.DIAMOND) {
diamonds += item.getAmount();
} else if (item.getType() == Material.EMERALD) {
emeralds += item.getAmount();
}
}
if (diamonds != 2 || emeralds != 3 || ((Chest) block.getState()).getLootTable() != null) {
throw new IllegalStateException("Loot retry failed: diamonds=" + diamonds + " emeralds=" + emeralds);
}
}
private void verifyLocateEvents(World world) {
Location origin = new Location(world, 2, 161, 2);
locateEvents = 0;
try {
locateAction = LocateAction.CANCEL;
if (world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false) != null) {
throw new IllegalStateException("Cancelled structure search returned a target");
}
locateAction = LocateAction.REPLACE;
StructureSearchResult result = world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false);
if (result == null || result.getStructure() != Structure.DESERT_PYRAMID
|| result.getLocation().getBlockX() != 123 || result.getLocation().getBlockY() != 100
|| result.getLocation().getBlockZ() != 456 || locateEvents != 2) {
throw new IllegalStateException("Structure search ignored event result: " + result);
}
} finally {
locateAction = LocateAction.PASS;
}
}
private World requireWorld() {
World world = Bukkit.getWorld(WORLD_KEY);
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
throw new IllegalStateException("Treasure map fixture world is absent");
}
return world;
}
private enum LocateAction {
PASS, CANCEL, REPLACE
}
}
@@ -0,0 +1,61 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
const world = 'native-terrain-support'
const air = new Set(['air', 'cave_air', 'void_air'])
export default {
name: 'iris-native-terrain-support',
description: 'Verify native terrain fitting preserves buried caves and excludes trees from lowered ground.',
async run(context) {
const bot = context.bot
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Requires an isolated instance')
const position = bot.entity.position.clone()
const at = (x, y, z) => bot.blockAt(position.clone().set(x, y, z))
const teleport = async (x, y, z) => {
await context.command(`/minecraft:tp @s ${x + 0.5} ${y} ${z + 0.5}`, /Teleported/i, 60000)
await context.waitUntil(() => Math.abs(bot.entity.position.x - x - 0.5) < 1
&& Math.abs(bot.entity.position.z - z - 0.5) < 1 && at(x, y, z),
{ label: 'Coordinate chunk received', timeoutMs: 90000, intervalMs: 100 })
}
await context.step('enter the seeded overworld', async () => {
if (context.options.command !== 'reuse') {
await context.command(`/iris create name=${world} type=overworld seed=1337`,
/Successfully created your world/i, 240000)
}
await context.command(`/iris tp ${world}`)
await context.waitUntil(async () => {
const snapshot = await context.observe()
const player = snapshot.players.find(entry => entry.username === bot.username)
return snapshot.worlds.some(entry => entry.id === player?.world && entry.name.endsWith(world))
}, { label: 'Entered seeded overworld', timeoutMs: 90000, intervalMs: 100 })
await context.command('/minecraft:gamemode spectator @s', /Spectator Mode/i)
await context.command('/minecraft:seed', /Seed:.*1337/)
})
await context.step('preserve caves beneath the ruined portal terrain skirt', async () => {
await teleport(995, 44, -3611)
await context.waitUntil(() => at(990, 43, -3626) && at(992, 43, -3627),
{ label: 'Cave columns received', timeoutMs: 90000, intervalMs: 100 })
for (const [x, z, bottom, top] of [[990, -3626, 33, 54], [992, -3627, 34, 55]]) {
for (let y = bottom; y <= top; y++) {
const block = at(x, y, z)
context.expect(block && air.has(block.name), 'Native terrain fitting filled a buried cave',
{ x, y, z, material: block?.name })
}
await context.command(`/execute if block ${x} 43 ${z} minecraft:cave_air run say CAVE_PRESERVED_${x}`,
new RegExp(`CAVE_PRESERVED_${x}`))
}
})
await context.step('exclude the pine whose support is removed by village grading', async () => {
await teleport(-2601, 174, -7063)
await context.waitUntil(() => at(-2601, 167, -7063),
{ label: 'Pine column received', timeoutMs: 90000, intervalMs: 100 })
const block = at(-2601, 167, -7063)
context.expect(air.has(block.name), 'Tree remains above terrain removed by native grading',
{ x: -2601, y: 167, z: -7063, material: block.name })
await context.command('/execute if block -2601 167 -7063 minecraft:air run say PINE_EXCLUDED',
/PINE_EXCLUDED/)
})
}
}
@@ -0,0 +1,20 @@
{
"name": "Flat",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{"minHeight": 1, "maxHeight": 1, "palette": [{"block": "minecraft:lime_wool"}]},
{"minHeight": 128, "maxHeight": 128, "palette": [{"block": "minecraft:stone"}]}
],
"generators": [{"generator": "flat", "min": 32, "max": 32}],
"objects": [{
"place": ["smolshroom1"],
"chance": 1,
"density": 8,
"mode": "ORGANIC_STILT",
"rotation": {"enabled": false},
"scale": {"size": 0.75, "interpolation": "NONE"},
"translate": {"y": 6},
"stiltSettings": {"organicMaxScan": 8, "organicJitter": 0, "organicScratch": 0}
}]
}
@@ -0,0 +1,15 @@
{
"name": "Object Scaling Fixture",
"version": 1,
"mode": {"type": "OVERWORLD"},
"regions": ["flat"],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {"min": 0, "max": 128},
"logicalHeight": 128,
"fluidHeight": 0,
"bedrock": false,
"caveProfile": {"enabled": false}
}
@@ -0,0 +1,5 @@
{
"seed": 310,
"interpolator": {"function": "NONE", "horizontalScale": 1},
"composite": [{"seed": 310, "style": {"style": "FLAT"}}]
}
@@ -0,0 +1,6 @@
{
"name": "Flat",
"landBiomes": ["flat"],
"seaBiomes": ["flat"],
"shoreBiomes": ["flat"]
}
@@ -0,0 +1,105 @@
import { createHash } from 'node:crypto'
import { readFile, realpath } from 'node:fs/promises'
import path from 'node:path'
export default {
name: 'iris-object-scaling',
description: 'Verify fractional mushroom scaling retains its cap and uses its stem for organic supports.',
async run(context) {
const directory = await realpath(context.server.directory)
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Object scaling requires an isolated instance')
const pack = await realpath(path.join(directory, 'plugins', 'Iris', 'packs', 'object-scaling-fixture'))
context.expect(pack.startsWith(`${directory}${path.sep}`), 'Object scaling requires a local fixture pack')
const object = await readFile(path.join(pack, 'objects', 'smolshroom1.iob'))
const objectHash = createHash('sha256').update(object).digest('hex')
const biome = JSON.parse(await readFile(path.join(pack, 'biomes', 'flat.json'), 'utf8'))
const surfaceY = biome.generators[0].min
const originalStemY = surfaceY + biome.objects[0].translate.y
const expectedCapY = originalStemY + 1
const maximumExtension = biome.objects[0].stiltSettings.organicMaxScan
context.expect(surfaceY === 32 && expectedCapY === 39 && maximumExtension === 8,
'Expected the bounded organic support fixture', { surfaceY, expectedCapY, maximumExtension })
const world = context.options.command ?? 'object_scaling_probe'
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
await context.step('create the fractional mushroom fixture world', async () => {
const response = await context.command(`/iris create name=${world} type=object-scaling-fixture seed=1337`,
/Successfully created your world|World ready|Failed to create|Exception raised/i, 180000)
context.expect(/Successfully created your world|World ready/i.test(response), response)
await context.command(`/iris tp ${world}`)
await context.waitUntil(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world}` || current?.name.endsWith(world)
}, { label: 'Enter the scaling fixture world', timeoutMs: 60000, intervalMs: 100 })
await context.command('/minecraft:gamemode spectator @s')
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
{ label: 'Spectator mode', timeoutMs: 30000, intervalMs: 100 })
await context.command('/minecraft:tp @s 0.5 48 0.5', /Teleported/i, 30000)
})
const columns = []
await context.step('client sees one cap and continuous stem supports', async () => {
const position = context.bot.entity.position.clone()
await context.waitUntil(() => {
if (Math.abs(context.bot.entity.position.x - 0.5) > 1
|| Math.abs(context.bot.entity.position.z - 0.5) > 1) return false
for (const x of [-16, 0, 16, 31]) {
for (const z of [-16, 0, 16, 31]) {
for (const y of [0, 32, 63]) {
if (!context.bot.blockAt(position.set(x, y, z))) return false
}
}
}
return true
}, { label: 'Complete mushroom fixture chunks', timeoutMs: 60000, intervalMs: 100 })
for (let x = -16; x < 32; x++) {
for (let z = -16; z < 32; z++) {
const caps = []
const stems = []
for (let y = 1; y < 63; y++) {
const block = context.bot.blockAt(position.set(x, y, z))
context.expect(block !== null, 'Every sampled mushroom block must be loaded', { x, y, z })
if (block.name === 'red_mushroom_block') caps.push(y)
if (block.name === 'mushroom_stem') stems.push(y)
}
if (caps.length === 0) continue
context.expect(caps.length === 1, 'Mushroom cap was copied into its stem or support', { x, z, caps })
const capY = caps[0]
context.expect(capY === expectedCapY, 'Fractional scaling changed the expected mushroom height',
{ x, z, capY, expectedCapY })
const bottomY = stems[0]
const stemBlocks = stems.length
context.expect(bottomY >= originalStemY - maximumExtension && bottomY <= surfaceY + 1,
'Mushroom support must reach the surface within its configured extension limit',
{ x, z, capY, bottomY, surfaceY, maximumExtension, stems })
context.expect(stemBlocks === capY - bottomY
&& stems.every((y, index) => y === bottomY + index),
'Scaled mushroom must have a continuous stem from its cap to the ground', { x, z, capY, stems })
const supportY = bottomY - 1
const support = context.bot.blockAt(position.set(x, supportY, z))?.name
const above = context.bot.blockAt(position.set(x, capY + 1, z))?.name
context.expect(support === (supportY === surfaceY ? 'lime_wool' : 'stone'),
'Mushroom stem must terminate on the fixture terrain', { x, z, capY, supportY, support })
context.expect(above === 'air', 'Scaled mushroom has unexpected blocks above its cap', { x, z, capY, above })
columns.push({ x, z, surfaceY, capY, bottomY, stemBlocks,
buriedStemBlocks: Math.max(0, surfaceY - bottomY + 1) })
}
}
context.expect(columns.length >= 8, 'Expected at least eight scaled mushroom columns', { columns })
})
await context.step('server confirms the sampled stem and cap materials', async () => {
for (const [index, column] of columns.slice(0, 8).entries()) {
const { x, z, surfaceY, capY, bottomY } = column
const marker = `IRIS_SCALING_OK_${index}`
await context.command(`/minecraft:execute if block ${x} ${surfaceY + 1} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${bottomY} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${capY} ${z} minecraft:red_mushroom_block run minecraft:say ${marker}`,
new RegExp(marker), 10000)
}
})
context.report.objectScaling = { world, scale: 0.75, object: 'smolshroom1', objectHash,
surfaceY, maximumExtension, checkedColumns: columns.length,
serverConfirmedColumns: Math.min(8, columns.length), columns }
}
}
@@ -0,0 +1,82 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const worlds = [
{ name: 'seed-selection-default', seed: '1337', argument: '' },
{ name: 'seed-selection-positive', seed: '78264193', argument: ' seed=78264193' },
{ name: 'seed-selection-negative', seed: '-734829104958217364', argument: ' seed=-734829104958217364' },
{ name: 'seed-selection-zero', seed: '0', argument: ' seed=0' }
]
export default {
name: 'iris-seed-selection',
description: 'Verify omitted, positive, negative, and zero creation seeds against visible terrain and a cold restart.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Seed selection requires an isolated instance')
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
const restart = context.options.command === 'restart-check'
const stateFile = path.join(context.server.directory, '.seed-selection.json')
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
const processId = (await context.observe()).processId
if (restart) {
context.expect(saved.processId !== processId, 'Restart-check requires a cold server restart')
context.expect(saved.jarHash === jarHash, 'Restart-check requires the same Iris jar')
}
const outputs = {}
for (const world of worlds) {
await context.step(`${restart ? 'Reload' : 'Create'} ${world.name} with seed ${world.seed}`, async () => {
if (!restart) {
await context.command(`/iris create name=${world.name} type=cohesion${world.argument}`,
/Successfully created your world/i, 180000)
}
await context.command(`/iris tp ${world.name}`)
await wait(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world.name}` || current?.name.endsWith(world.name)
}, `Enter ${world.name}`)
await context.command('/minecraft:gamemode spectator @s')
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
await context.command('/minecraft:seed', new RegExp(`Seed:\\s*\\[?${world.seed}\\]?(?:\\s|$)`))
const x = 1024
const z = -1024
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Sample teleport')
const position = context.bot.entity.position.clone()
await wait(() => context.bot.blockAt(position.clone().set(x, 0, z))
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
const digest = createHash('sha256')
let stone = 0
for (let dx = 0; dx < 16; dx++) {
for (let dz = 0; dz < 16; dz++) {
for (let y = 0; y < 64; y++) {
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
digest.update(`${block.stateId},`)
if (block.name === 'stone') stone++
}
}
}
context.expect(stone > 1000, 'Fixture terrain was not generated', { world: world.name, stone })
const blocks = digest.digest('hex')
outputs[world.name] = { seed: world.seed, blocks }
if (restart) {
context.expect(saved.worlds[world.name].seed === world.seed, 'Seed expectation changed after restart')
context.expect(saved.worlds[world.name].blocks === blocks, 'Persisted terrain changed after restart',
{ world: world.name, before: saved.worlds[world.name].blocks, after: blocks })
}
})
}
const signatures = new Set(Object.values(outputs).map(output => output.blocks))
context.expect(signatures.size === worlds.length, 'Different requested seeds generated identical sample chunks', outputs)
context.report.seedSelection = { restart, jarHash, worlds: outputs }
if (!restart) {
await writeFile(stateFile, JSON.stringify({ processId, jarHash, worlds: outputs }))
}
}
}
@@ -0,0 +1,71 @@
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
logPosition, logSince } from './studio-support.mjs'
export default {
name: 'iris-studio-hotload-lifecycle',
description: 'Generate cold chunks during Studio edits and close after saving an edit.',
async run(context) {
const fixture = await setup(context)
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
const original = await readFile(biomeFile, 'utf8')
const logOffset = await logPosition(fixture)
let active
let x = 16384
try {
active = await openStudio(context, fixture)
await sampleTerrain(context, fixture, 0, fixture.state.material)
for (let cycle = 0; cycle < 3; cycle++) {
await context.step(`Load cold chunks during hotload ${cycle + 1}`, async () => {
const manifestFile = fixture.manifestPath(active.key)
const before = await readJson(manifestFile)
const biome = await readJson(biomeFile)
const material = cycle % 2 === 0 ? 'gold_block' : 'diamond_block'
biome.name = `Lifecycle terrain ${cycle + 1}`
biome.layers[0].palette[0].block = `minecraft:${material}`
const startedAt = Date.now()
await writeJson(biomeFile, biome)
x += 512
await context.command(`/minecraft:tp @s ${x + 0.5} 101 0.5`, /Teleported/i, 15000)
const after = await fixture.wait(async () => {
const manifest = await readJson(manifestFile)
return manifest.activeActivationId !== before.activeActivationId ? manifest : false
}, 'Hotload did not complete while the player requested fresh chunks', 90000)
const activationDurationMs = Date.now() - startedAt
await fixture.wait(() => Math.abs(context.bot.entity.position.x - x - 0.5) < 1,
'Cold chunk teleport did not arrive after hotload', 30000)
context.report.iris.activations.push({ before: before.activeActivationId,
after: after.activeActivationId, material, coldChunkRequests: 1, activationDurationMs })
x += 2048
await sampleTerrain(context, fixture, x, material)
})
}
await context.step('Close Studio while a saved edit is activating', async () => {
const biome = await readJson(biomeFile)
biome.name = 'Lifecycle close terrain'
await writeJson(biomeFile, biome)
const pending = await fixture.wait(async () => {
const manifest = await readJson(fixture.manifestPath(active.key))
return manifest.pendingActivationId !== null ? manifest : false
}, 'The saved close edit did not enter activation', 90000)
context.report.iris.closePendingActivationId = pending.pendingActivationId
await closeStudio(context, fixture, active)
active = undefined
await context.command('/minecraft:seed', /Seed:/i, 10000)
})
await context.step('Reopen the edited pack and retain usable generation', async () => {
active = await openStudio(context, fixture)
await sampleTerrain(context, fixture, 0, 'gold_block')
await closeStudio(context, fixture, active)
active = undefined
})
const log = await logSince(fixture, logOffset)
context.expect(!/Studio generation update failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
'Studio lifecycle emitted a generation, snapshot, storage, or watchdog failure')
} finally {
if (active) await closeStudio(context, fixture, active)
await writeFile(biomeFile, original)
}
}
}
@@ -0,0 +1,33 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"destination": "iris_qa:blocked",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"function": "minecraft:exploration_map"
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,33 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"destination": "iris_qa:disabled",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"function": "minecraft:exploration_map"
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,33 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"destination": "iris_qa:found",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"function": "minecraft:exploration_map"
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,33 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"destination": "iris_qa:mixed",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"function": "minecraft:exploration_map"
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,14 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:buried_treasure"
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:mansion"
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:desert_pyramid"
]
}
@@ -0,0 +1,6 @@
{
"values": [
"minecraft:buried_treasure",
"minecraft:desert_pyramid"
]
}
@@ -0,0 +1,13 @@
{
"pack": {
"description": "Treasure map gameplay fixtures",
"min_format": [
107,
1
],
"max_format": [
107,
1
]
}
}
@@ -0,0 +1,31 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:filled_map",
"modifier": {
"destination": "#iris_qa:blocked",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"type": "minecraft:exploration_map"
}
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,31 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:filled_map",
"modifier": {
"destination": "#iris_qa:disabled",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"type": "minecraft:exploration_map"
}
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,31 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:filled_map",
"modifier": {
"destination": "#iris_qa:found",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"type": "minecraft:exploration_map"
}
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,31 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:filled_map",
"modifier": {
"destination": "#iris_qa:mixed",
"decoration": "minecraft:red_x",
"zoom": 0,
"search_radius": 50,
"skip_existing_chunks": false,
"type": "minecraft:exploration_map"
}
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,14 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond"
}
]
}
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:buried_treasure"
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:mansion"
]
}
@@ -0,0 +1,5 @@
{
"values": [
"minecraft:desert_pyramid"
]
}
@@ -0,0 +1,6 @@
{
"values": [
"minecraft:buried_treasure",
"minecraft:desert_pyramid"
]
}
@@ -0,0 +1,13 @@
{
"pack": {
"description": "Treasure map gameplay fixtures",
"min_format": [
121,
0
],
"max_format": [
121,
0
]
}
}
@@ -0,0 +1,32 @@
{
"name": "Flat",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{
"minHeight": 1,
"maxHeight": 1,
"palette": [
{
"block": "minecraft:lime_wool"
}
]
},
{
"minHeight": 384,
"maxHeight": 384,
"palette": [
{
"block": "minecraft:green_concrete"
}
]
}
],
"generators": [
{
"generator": "flat",
"min": 96,
"max": 96
}
]
}
@@ -0,0 +1,64 @@
{
"name": "Treasure Map Fixture",
"version": 1,
"mode": {
"type": "OVERWORLD"
},
"regions": [
"flat"
],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {
"min": -64,
"max": 320
},
"logicalHeight": 384,
"fluidHeight": 0,
"bedrock": false,
"rockPalette": {
"palette": [
{
"block": "minecraft:red_concrete"
}
]
},
"importedStructures": {
"disabled": [
"minecraft:"
]
},
"structures": [
{
"placementId": "blocked-treasure",
"nativeStructures": [
{
"structure": "minecraft:buried_treasure"
}
],
"distribution": "RANDOM_SPREAD",
"spacing": 1,
"separation": 0,
"minHeight": 0,
"maxHeight": 0
},
{
"placementId": "reachable-pyramid",
"nativeStructures": [
{
"structure": "minecraft:desert_pyramid"
}
],
"distribution": "RANDOM_SPREAD",
"spacing": 16,
"separation": 4
}
],
"loot": {
"tables": [
"fixture-diamond"
]
}
}
@@ -0,0 +1,15 @@
{
"interpolator": {
"function": "NONE",
"horizontalScale": 1
},
"seed": 310,
"composite": [
{
"seed": 310,
"style": {
"style": "FLAT"
}
}
]
}
@@ -0,0 +1,15 @@
{
"name": "Fixture Diamond",
"rarity": 1,
"minPicked": 1,
"maxPicked": 1,
"maxTries": 1,
"loot": [
{
"type": "diamond",
"rarity": 1,
"minAmount": 1,
"maxAmount": 1
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Flat",
"landBiomes": [
"flat"
],
"seaBiomes": [
"flat"
],
"shoreBiomes": [
"flat"
]
}
@@ -0,0 +1,211 @@
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const simplifyNbt = value => {
if (Array.isArray(value)) return value.map(simplifyNbt)
if (value === null || typeof value !== 'object') return value
if ('type' in value && 'value' in value) return simplifyNbt(value.value)
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, simplifyNbt(entry)]))
}
const completeClientTicks = context => {
const client = context.bot._client
context.expect(context.bot.supportFeature('sendsClientTickEndPacket'), 'Fixture client supports tick delimiters')
let tickTimer
const finishTick = () => {
clearTimeout(tickTimer)
client.write('tick_end', {})
tickTimer = setTimeout(finishTick, 50)
}
context.bot.on('move', finishTick)
finishTick()
context.signal.addEventListener('abort', () => {
clearTimeout(tickTimer)
context.bot.removeListener('move', finishTick)
}, { once: true })
}
export default {
name: 'iris-treasure-maps',
description: 'Verify exploration-map targets, bounded misses, retained loot, and normal/copper storage on Iris.',
async run(context) {
completeClientTicks(context)
context.bot.physicsEnabled = false
context.bot.clearControlStates()
context.bot.entity.velocity.set(0, 0, 0)
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Treasure map fixtures require an isolated instance')
const command = async (operation, expected, timeoutMs = 30000) => {
const token = expected.split(' ')[0]
const response = await context.command(`/irislifecycle ${operation}`,
new RegExp(`${token}|NATIVE_FAILED|TREASURE_.*_FAILED`), timeoutMs)
context.expect(response.includes(expected), response)
return response
}
const bounded = (label, action) => context.waitUntil(async () => {
await action()
return true
}, { label, timeoutMs: 10000 })
let lastActivation = 0
const openChest = async material => {
const position = context.bot.entity.position.clone().set(2, 161, 2)
await context.waitUntil(() => context.bot.blockAt(position)?.name === material, {
label: `${material} reached the client`, timeoutMs: 10000
})
const delay = 150 - (performance.now() - lastActivation)
if (delay > 0) await context.sleep(delay)
const opened = context.waitForEvent('windowOpen', undefined, 30000)
await bounded(`Activate ${material}`, () => context.bot.activateBlock(context.bot.blockAt(position)))
lastActivation = performance.now()
const [window] = await opened
return window
}
const chestItems = window => window.slots.slice(0, window.inventoryStart).filter(Boolean)
const mapId = item => item.componentMap?.get('map_id')?.data
let missItem
await context.step('create and enter the Iris treasure map world', async () => {
const response = await command('treasure-map-create', 'TREASURE_MAP_READY world=iris:treasure_map_probe', 180000)
const version = response.match(/version=(\S+)/)?.[1]
context.expect(/^26\.[23](?:[.-]|$)/.test(version ?? ''), 'Fixture requires its matching 26.2 or 26.3 data pack', { version })
missItem = version.startsWith('26.2') ? 'map' : 'filled_map'
context.report.treasurePlatform = { version, folia: response.includes('folia=true'), missItem }
await context.waitUntil(() => context.bot.entity.position.distanceTo(
context.bot.entity.position.clone().set(0.5, 161, 0.5)
) < 2, { label: 'Fixture teleport reached the client', timeoutMs: 30000 })
})
await context.step('confirm the missing target exhausts the placement search budget', async () => {
await command('treasure-map-limit', 'TREASURE_MAP_LIMIT status=SEARCH_LIMIT_REACHED', 120000)
})
for (const material of ['chest', 'copper_chest']) {
for (const destination of ['blocked', 'disabled', 'found', 'mixed']) {
await context.step(`open ${material} with ${destination} map target and retain all loot`, async () => {
const evidence = { destination, material, phase: 'prepare' }
context.report.treasureMaps ??= []
context.report.treasureMaps.push(evidence)
const located = destination === 'found' || destination === 'mixed'
const mapName = located ? 'filled_map' : missItem
await command(`treasure-map-chest ${destination} ${material.toUpperCase()}`,
`TREASURE_MAP_CHEST destination=${destination} material=${material.toUpperCase()}`)
evidence.phase = 'open'
const window = await openChest(material)
evidence.windowType = window.type
evidence.inventoryStart = window.inventoryStart
let targetId
let diamondType
let diamondsBefore
let marker
let verified
try {
await context.waitUntil(() => {
const items = chestItems(window)
return items.some(item => item.name === mapName) && items.some(item => item.name === 'diamond')
}, { label: `${destination} chest contains map and diamond`, timeoutMs: 30000 })
const items = chestItems(window)
const map = items.find(item => item.name === mapName)
const diamond = items.find(item => item.name === 'diamond')
targetId = mapId(map)
context.expect(items.length === 2 && map.count === 1 && diamond.count === 1,
'Loot table must produce exactly one map and one diamond')
context.expect((targetId !== undefined) === located, 'Only reachable destinations receive map data', {
destination, components: map.components
})
const response = await command(`treasure-map-inspect ${destination}`,
`TREASURE_MAP_INSPECT destination=${destination} diamonds=1 located=${located}`, 120000)
verified = Object.fromEntries([...response.matchAll(/(mapId|targetX|targetZ|centerX|centerZ)=(-?\d+)/g)]
.map(([, key, value]) => [key, Number(value)]))
const decorations = simplifyNbt(map.componentMap?.get('map_decorations')?.data)
marker = decorations?.['+']
if (located) {
context.expect(targetId === verified.mapId, 'Client and saved map IDs agree', { targetId, verified })
context.expect(marker?.type === 'minecraft:red_x'
&& marker.x === verified.targetX && marker.z === verified.targetZ,
'Map marker points to the verified pyramid', { marker, verified, components: map.components })
} else {
context.expect(marker === undefined, 'Missing destinations must not invent a map marker', { decorations })
}
diamondType = diamond.type
diamondsBefore = context.bot.inventory.count(diamondType)
evidence.phase = 'withdraw'
evidence.diamondSlot = diamond.slot
await bounded(`Withdraw diamond from ${material} ${destination}`,
() => context.bot.clickWindow(diamond.slot, 0, 1))
evidence.phase = 'withdrawn'
await context.waitUntil(() => chestItems(window).every(item => item.name !== 'diamond'), {
label: 'Diamond transferred out of the chest', timeoutMs: 10000
})
} finally {
context.bot.closeWindow(window)
}
evidence.phase = 'reopen'
const reopened = await openChest(material)
try {
await context.waitUntil(() => chestItems(reopened).some(item => item.name === mapName), {
label: 'Saved map remains in the reopened chest', timeoutMs: 10000
})
const retained = chestItems(reopened)
context.expect(retained.length === 1 && retained[0].name === mapName && retained[0].count === 1,
'Reopening must not regenerate chest loot')
context.expect(mapId(retained[0]) === targetId, 'Reopening must retain the same map data')
context.expect(JSON.stringify(simplifyNbt(retained[0].componentMap?.get('map_decorations')?.data)?.['+'])
=== JSON.stringify(marker), 'Reopening must retain the same destination marker')
await context.waitUntil(() => context.bot.inventory.count(diamondType) === diamondsBefore + 1, {
label: 'Chest awards exactly one diamond', timeoutMs: 10000
})
Object.assign(evidence, { phase: 'complete', mapItem: mapName,
mapId: targetId ?? null, marker: marker ?? null, verified, reopened: true })
} finally {
context.bot.closeWindow(reopened)
}
})
}
}
await context.step('verify storage variants, insertion rollback, and locate event results', async () => {
await command('treasure-extra-check', 'TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true', 120000)
})
await context.step('verify plugin loot runs on its owning region', async () => {
await command('treasure-region-check', 'TREASURE_REGION_LOOT copper=8 normal=1 events=9 owned=true', 120000)
})
await context.step('verify native loot table reload support', async () => {
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
if (context.report.treasurePlatform.folia) {
const response = await context.command('/minecraft:reload', /Unknown or incomplete command|Reloading/i, 10000)
context.expect(response.includes('Unknown or incomplete command'),
'Folia fixture expects its unavailable data pack reload command', { response })
context.report.treasureReload = { status: 'unavailable', before: 'diamond', evidence: response }
return
}
const properties = await readFile(path.join(context.server.directory, 'server.properties'), 'utf8')
const levelName = properties.match(/^level-name=(.+)$/m)?.[1].trim() ?? 'world'
const tablePath = path.join(context.server.directory, levelName, 'datapacks', 'iris_treasure',
'data', 'iris_qa', 'loot_table', 'reload.json')
const original = await readFile(tablePath, 'utf8')
const replacement = JSON.parse(original)
context.expect(replacement.pools[0].entries[0].name === 'minecraft:diamond',
'Reload fixture must start with its diamond table')
const revision = async () => {
const response = await command('treasure-resources', 'TREASURE_RESOURCES revision=')
return Number(response.match(/revision=(\d+)/)?.[1])
}
const reload = async () => {
const before = await revision()
await context.command('/minecraft:reload', /Reloading/i, 30000)
await context.waitUntil(async () => await revision() > before, {
label: 'Server resource reload completed', timeoutMs: 120000, intervalMs: 500
})
}
replacement.pools[0].entries[0].name = 'minecraft:emerald'
try {
await writeFile(tablePath, `${JSON.stringify(replacement, null, 2)}\n`)
await reload()
await command('treasure-reload-after', 'TREASURE_RELOAD material=EMERALD')
context.report.treasureReload = { status: 'passed', before: 'diamond', after: 'emerald' }
} finally {
await writeFile(tablePath, original)
if (!context.signal.aborted) {
await reload()
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
}
}
})
}
}
@@ -0,0 +1,143 @@
import { readFile, realpath } from 'node:fs/promises'
import path from 'node:path'
const nonGround = new Set(['air', 'cave_air', 'void_air', 'water', 'lava', 'nether_wart_block', 'warped_wart_block'])
const chunkOffsets = [-56, -40, -24, -8, 8, 24, 40, 56]
function isWood(block) {
return block !== null && /_(wood|hyphae|log|stem)$/.test(block.name)
}
function isGround(block) {
return block !== null && block.boundingBox === 'block' && !nonGround.has(block.name)
&& !block.name.endsWith('_leaves') && !isWood(block)
}
function positionKey(position) {
return `${position.x},${position.y},${position.z}`
}
function groundSpread(bot, base) {
const heights = []
for (const x of [-3, 0, 3]) {
for (const z of [-3, 0, 3]) {
for (let y = base.y + 6; y >= base.y - 8; y--) {
if (!isGround(bot.blockAt(base.offset(x, y - base.y, z)))) continue
heights.push(y)
break
}
}
}
return heights.length === 9 ? Math.max(...heights) - Math.min(...heights) : null
}
function inspectTrees(context) {
const bot = context.bot
const center = bot.entity.position.floored()
const woodIds = Object.values(bot.registry.blocksByName).filter(isWood).map(block => block.id)
const positions = bot.findBlocks({ matching: woodIds, point: center, maxDistance: 64, count: 20000 })
const columns = new Map()
for (const position of positions) {
const key = `${position.x},${position.z}`
if (!columns.has(key)) columns.set(key, [])
columns.get(key).push(position)
}
const trees = []
for (const column of columns.values()) {
column.sort((first, second) => first.y - second.y)
const base = column[0]
const height = column[column.length - 1].y - base.y + 1
if (column.length < 8 || height < 8 || column.length / height < 0.65) continue
if (column.some(position => position.distanceTo(center) >= 60)) continue
if (![0, 1, 2, 3].every(y => isWood(bot.blockAt(base.offset(0, y, 0))))) continue
const upperGaps = []
for (let y = base.y; y < base.y + height; y++) {
const block = bot.blockAt(base.offset(0, y - base.y, 0))
if (block === null || !isWood(block)) upperGaps.push({ y, block: block?.name ?? null })
}
const grounded = isGround(bot.blockAt(base.offset(0, -1, 0)))
const spread = groundSpread(bot, base)
trees.push({ base: { x: base.x, y: base.y, z: base.z }, woodBlocks: column.length,
height, grounded, terrainSpread: spread, upperGaps })
}
return trees
}
export default {
name: 'iris-tree-grounding',
description: 'Verify grounded spindle bases, continuous overlapping trunks, and paired pack geometry.',
async run(context) {
const directory = await realpath(context.server.directory)
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Tree generation requires an isolated instance')
const packs = await realpath(path.join(directory, 'plugins', 'Iris', 'packs'))
context.expect(packs.startsWith(`${directory}${path.sep}`), 'Tree tests require local pack copies')
context.report.treeWorlds = []
context.report.treeGrounding = []
context.report.treeContinuity = []
for (const pack of ['overworld', 'underworld']) {
const dimension = JSON.parse(await readFile(path.join(packs, pack, 'dimensions', `${pack}.json`), 'utf8'))
context.expect(dimension.focus === 'estranged/spindlewood', 'Pack must focus on spindlewood', { pack })
await context.step(`create ${pack} spindle world`, async () => {
const world = `tree_${pack}_${Date.now().toString(36)}`
const response = await context.command(`/iris create name=${world} type=${pack} seed=78264193`,
/Successfully created your world|Failed to create/, 240000)
context.expect(response.includes('Successfully created your world'), response)
context.report.treeWorlds.push({ pack, name: world, seed: 78264193, biome: dimension.focus })
if (context.bot.game.gameMode !== 'spectator') {
await context.command('/minecraft:gamemode spectator', /spectator/i, 30000)
}
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
{ label: 'Spectator mode for terrain inspection', timeoutMs: 30000, intervalMs: 100 })
})
await context.step(`${pack} trees connect to flat and sloped terrain`, async () => {
const origin = context.bot.entity.position.floored()
const inspected = new Map()
for (const offset of [0, 80, 160]) {
await context.command(`/minecraft:tp @s ${origin.x + offset + 0.5} ${origin.y + 12} ${origin.z + 0.5}`,
/Teleported/i, 30000)
await context.waitUntil(() => {
const center = context.bot.entity.position.floored()
if (Math.abs(center.x - origin.x - offset) > 1) return false
return chunkOffsets.every(x => chunkOffsets.every(z => context.bot.blockAt(center.offset(x, 0, z)) !== null))
}, { label: 'Complete terrain neighborhood', timeoutMs: 90000, intervalMs: 250 })
const trees = inspectTrees(context)
for (const tree of trees) {
context.expect(tree.grounded, 'Generated spindle trunk base has no terrain connection', { pack, ...tree })
inspected.set(positionKey(tree.base), tree)
}
if (inspected.size >= 8 && [...inspected.values()].some(tree => tree.terrainSpread >= 2)) break
}
const trees = [...inspected.values()]
const sloped = trees.filter(tree => tree.terrainSpread >= 2)
context.expect(trees.length >= 8, 'Expected at least eight generated spindle trunk bases', { pack, trees })
context.expect(sloped.length > 0, 'Expected a grounded tree on uneven terrain', { pack, trees })
context.report.treeGrounding.push({ pack, trees: trees.length, sloped: sloped.length, samples: trees })
})
await context.step(`${pack} overlapping canopy preserves the seeded main trunk`, async () => {
await context.command('/minecraft:tp @s 0.5 88 0.5', /Teleported/i, 30000)
await context.waitUntil(() => {
const center = context.bot.entity.position.floored()
return Math.abs(center.x) < 2 && Math.abs(center.z) < 2
&& context.bot.blockAt(center.offset(-7 - center.x, 69 - center.y, 6 - center.z)) !== null
}, { label: 'Seeded trunk neighborhood', timeoutMs: 90000, intervalMs: 250 })
const center = context.bot.entity.position.floored()
const blocks = []
for (let y = 69; y <= 83; y++) {
const block = context.bot.blockAt(center.offset(-7 - center.x, y - center.y, 6 - center.z))
context.expect(isWood(block), 'Overlapping canopy interrupted the seeded main trunk',
{ pack, x: -7, y, z: 6, block: block?.name ?? null })
blocks.push({ y, block: block.name })
}
context.report.treeContinuity.push({ pack, x: -7, z: 6, blocks })
})
}
await context.step('paired packs retain matching sampled tree geometry', async () => {
const geometry = context.report.treeGrounding.map(result => result.samples.map(tree => ({
base: tree.base, woodBlocks: tree.woodBlocks, height: tree.height, terrainSpread: tree.terrainSpread
})).sort((first, second) => positionKey(first.base).localeCompare(positionKey(second.base))))
context.expect(JSON.stringify(geometry[0]) === JSON.stringify(geometry[1]),
'Paired spindle packs produced different sampled geometry', { geometry })
})
}
}
@@ -0,0 +1,230 @@
package art.arcane.iris;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.GenerationSessionManager;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.WorldMaintenance;
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisShutdownMaintenanceTest {
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
@Test
public void finisherRetainsResourcesWhileTheAuthoritativeBoundaryIsPending() throws Exception {
try (Fixture fixture = new Fixture()) {
ServerShutdownBoundary boundary = mock(ServerShutdownBoundary.class);
CountDownLatch waiting = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicReference<Thread> finisher = new AtomicReference<>();
when(boundary.await(anyLong(), eq(TimeUnit.SECONDS))).thenAnswer(invocation -> {
finisher.set(Thread.currentThread());
waiting.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return false;
});
setField(fixture.plugin, "serverShutdownBoundary", boundary);
fixture.quiesce();
fixture.startFinisher();
try {
assertTrue(waiting.await(5, TimeUnit.SECONDS));
fixture.startFinisher();
verify(fixture.generator, never()).closeAsync();
verify(fixture.binding, never()).releasePluginClassLoaderClose();
} finally {
release.countDown();
Thread running = finisher.get();
if (running != null) {
running.join(5_000L);
assertFalse(running.isAlive());
}
}
verify(boundary, times(1)).await(anyLong(), eq(TimeUnit.SECONDS));
verify(fixture.generator, never()).closeAsync();
}
}
@Test
public void missingShutdownBoundaryDoesNotAuthorizeResourceRelease() throws Exception {
try (Fixture fixture = new Fixture()) {
Method method = Iris.class.getDeclaredMethod("awaitServerShutdownBoundary");
method.setAccessible(true);
assertEquals(false, method.invoke(fixture.plugin));
}
}
@Test
public void serverStopKeepsFoliaGenerationAdmittedUntilTheServerDrains() throws Exception {
try (Fixture fixture = new Fixture()) {
GenerationSessionManager sessions = new GenerationSessionManager();
try (GenerationSessionLease active = sessions.acquireForEngine(fixture.engine, "active_chunk")) {
fixture.quiesce();
assertTrue(WorldMaintenance.isWorldMaintenanceActive(fixture.identity));
assertFalse(fixture.hooks.shouldRefuseGeneration(fixture.engine));
try (GenerationSessionLease queued = sessions.acquireForEngine(fixture.engine, "queued_chunk")) {
assertEquals(active.sessionId(), queued.sessionId());
assertEquals(2, sessions.activeLeases());
}
verify(fixture.generator).quiesceForServerShutdown();
verify(fixture.generator, never()).closeAsync();
fixture.nms.verify(INMS::get);
verify(fixture.binding).deferPluginClassLoaderClose();
verify(fixture.binding, never()).releasePluginClassLoaderClose();
}
assertEquals(0, sessions.activeLeases());
fixture.drain();
verify(fixture.generator).closeAsync();
}
}
@Test
public void serverStopDoesNotOverrideExistingBlockingMaintenance() throws Exception {
try (Fixture fixture = new Fixture()) {
WorldMaintenance.beginWorldMaintenance(fixture.identity, "world_unload", true);
try {
fixture.quiesce();
assertTrue(fixture.hooks.shouldRefuseGeneration(fixture.engine));
} finally {
WorldMaintenance.endWorldMaintenance(fixture.identity, "world_unload", true);
}
assertFalse(fixture.hooks.shouldRefuseGeneration(fixture.engine));
}
}
@Test
public void manualDisableStillBlocksGenerationBeforeClosingTheGenerator() throws Exception {
try (Fixture fixture = new Fixture()) {
when(fixture.generator.closeAsync()).thenAnswer(invocation -> {
assertTrue(fixture.hooks.shouldRefuseGeneration(fixture.engine));
return CompletableFuture.completedFuture(null);
});
try {
fixture.drain();
assertTrue(fixture.hooks.shouldRefuseGeneration(fixture.engine));
verify(fixture.generator).closeAsync();
verify(fixture.generator, never()).quiesceForServerShutdown();
} finally {
WorldMaintenance.endWorldMaintenance(fixture.identity, "manual_disable", true);
}
}
}
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
Field field = Iris.class.getDeclaredField(name);
field.setAccessible(true);
field.set(plugin, value);
}
private static final class Fixture implements AutoCloseable {
private final Iris plugin = mock(Iris.class);
private final World world = mock(World.class);
private final IrisWorld irisWorld = mock(IrisWorld.class);
private final Engine engine = mock(Engine.class);
private final PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
private final BukkitEnginePlatformHooks hooks = new BukkitEnginePlatformHooks();
private final INMSBinding binding = mock(INMSBinding.class);
private final String identity = "iris:shutdown_test";
private final IrisSettings previousSettings = IrisSettings.settings;
private final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
private final MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class, CALLS_REAL_METHODS);
private final MockedStatic<BukkitWorldBinding> worlds = mockStatic(BukkitWorldBinding.class);
private final MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
private final MockedStatic<INMS> nms = mockStatic(INMS.class);
private final MockedStatic<J> tasks = mockStatic(J.class, CALLS_REAL_METHODS);
private final MockedStatic<PregeneratorJob> pregenerator = mockStatic(PregeneratorJob.class);
private int quiesces;
private Fixture() throws ReflectiveOperationException {
IrisSettings.settings = new IrisSettings();
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
when(world.getKey()).thenReturn(NamespacedKey.fromString(identity));
when(engine.getWorld()).thenReturn(irisWorld);
when(irisWorld.hasPlatformWorld()).thenReturn(true);
when(irisWorld.identity()).thenReturn(identity);
when(generator.closeAsync()).thenReturn(CompletableFuture.completedFuture(null));
bukkit.when(Bukkit::getWorlds).thenReturn(List.of(world));
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
worlds.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
nms.when(INMS::get).thenReturn(binding);
tasks.when(J::isFolia).thenReturn(true);
}
private void quiesce() throws ReflectiveOperationException {
Method method = Iris.class.getDeclaredMethod("quiesceRuntimeForServerShutdown", String.class);
method.setAccessible(true);
method.invoke(plugin, "server_stop");
quiesces++;
}
private void drain() throws ReflectiveOperationException {
Method method = Iris.class.getDeclaredMethod("drainWorldGenerators", String.class, long.class);
method.setAccessible(true);
method.invoke(plugin, "manual_disable", 5L);
}
private void startFinisher() throws ReflectiveOperationException {
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
method.setAccessible(true);
method.invoke(plugin);
}
@Override
public void close() {
for (int index = 0; index < quiesces; index++) {
WorldMaintenance.endWorldMaintenance(identity, "server_stop");
}
pregenerator.close();
tasks.close();
nms.close();
services.close();
worlds.close();
toolbelt.close();
bukkit.close();
IrisSettings.settings = previousSettings;
}
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris;
import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginDescriptionFile;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.InputStream;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class IrisVersionNumberTest {
@Test
public void releaseAndQualifiedBuildsUseTheSameVersionNumber() {
for (String version : List.of("4.3.3", "4.3.3-SNAPSHOT", "4.3.3-test")) {
assertEquals(version, 433, plugin(version).getIrisVersion());
}
}
@Test
public void processedPluginMetadataProvidesAValidEngineVersion() throws Exception {
try (InputStream stream = getClass().getResourceAsStream("/plugin.yml")) {
assertNotNull(stream);
PluginDescriptionFile description = new PluginDescriptionFile(stream);
Iris plugin = mock(Iris.class, CALLS_REAL_METHODS);
doReturn(description).when(plugin).getDescription();
assertNotEquals(-1, plugin.getIrisVersion());
}
}
@Test
public void malformedAndOverflowingReleaseNumbersStayInvalid() {
for (String version : List.of("development", "4..3", ".4.3.3", "4.3.3.", "4.3a.3-test",
"2147483648", "999999999999999999999-SNAPSHOT", "-SNAPSHOT")) {
assertEquals(version, -1, plugin(version).getIrisVersion());
}
}
@Test
public void supportedMinecraftVersionsRetainTheirEngineNumbers() {
Iris plugin = mock(Iris.class, CALLS_REAL_METHODS);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::getVersion).thenReturn("26.1.2-74-ver/26.1.2@e4e17fc (MC: 26.1.2)");
assertEquals(2612, plugin.getMCVersion());
bukkit.when(Bukkit::getVersion).thenReturn("26.1.2-8-ver/26.1.x@62dc0f2 (MC: 26.1.2)");
assertEquals(2612, plugin.getMCVersion());
bukkit.when(Bukkit::getVersion).thenReturn("26.2-133 (MC: 26.2)");
assertEquals(262, plugin.getMCVersion());
bukkit.when(Bukkit::getVersion).thenReturn("26.3-169 (MC: 26.3)");
assertEquals(263, plugin.getMCVersion());
}
}
private static Iris plugin(String version) {
Iris plugin = mock(Iris.class, CALLS_REAL_METHODS);
doReturn(new PluginDescriptionFile("Iris", version, Iris.class.getName())).when(plugin).getDescription();
return plugin;
}
}
@@ -20,33 +20,20 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest {
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
private static final List<String> JOINED_PLUGIN_IDS = List.of(
"PlaceholderAPI",
"CraftEngine",
"Nexo",
"Oraxen",
"ItemsAdder",
"SCore",
"ExecutableItems",
"MythicLib",
"MMOItems",
"eco",
"EcoItems",
"MythicMobs",
"MythicCrucible",
"KGenerators",
"Multiverse-Core",
"WorldEdit"
);
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
.toList();
private static final List<String> UNORDERED_PLUGIN_IDS = List.of(
"MythicLib", "MMOItems", "MythicMobs", "MythicCrucible");
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
.filter(pluginId -> !UNORDERED_PLUGIN_IDS.contains(pluginId))
.toList();
@Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
@@ -75,10 +62,13 @@ public class PaperPluginMetadataTest {
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
+ " default: op"));
for (String pluginId : JOINED_PLUGIN_IDS) {
String loadOrder = UNORDERED_PLUGIN_IDS.contains(pluginId) ? "OMIT" : "BEFORE";
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, loadOrder, true)));
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "OMIT", true)));
}
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
assertFalse(metadata.contains(" ExecutableItems:"));
assertFalse(metadata.contains(" EcoItems:"));
assertFalse(metadata.contains(" MythicLib:"));
assertEquals(1, metadata.lines().filter(line -> line.equals(" load: AFTER")).count());
assertEquals(0, metadata.lines().filter(line -> line.equals(" load: BEFORE")).count());
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
}
@@ -95,7 +85,7 @@ public class PaperPluginMetadataTest {
Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertTrue(metadata.getSoftDepend().isEmpty());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
Permission all = metadata.getPermissions().stream()
.filter(permission -> "iris.all".equals(permission.getName()))
@@ -5,19 +5,66 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import org.junit.Test;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicesManager;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisIntegrationServiceTest {
@Test
public void disableClearsPublishedSnapshots() {
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
Set<String> keys = Set.of(IntegrationMetricSchema.IRIS_WORLD_COUNT);
service.snapshotMetrics(keys);
service.publishSnapshots();
long generation = service.snapshotMetrics(keys).generation();
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::getServicesManager).thenReturn(Mockito.mock(ServicesManager.class));
service.onDisable();
}
assertTrue(service.snapshotMetrics(keys).samples().isEmpty());
assertTrue(service.snapshotMetrics(keys).generation() > generation);
}
@Test
public void snapshotRequestsDoNotCollectAndKeepOriginalTelemetryTime() {
long sampledAt = System.currentTimeMillis() - 5_000L;
IrisTelemetrySnapshot source = telemetry(List.of(), IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAt);
AtomicInteger reads = new AtomicInteger();
IrisIntegrationService service = new IrisIntegrationService(() -> {
reads.incrementAndGet();
return source;
});
Set<String> keys = Set.of(IntegrationMetricSchema.IRIS_WORLD_COUNT);
assertTrue(service.capabilities().contains(IntegrationSnapshotProvider.CAPABILITY));
service.publishSnapshots();
assertEquals(0, reads.get());
assertTrue(service.snapshotMetrics(keys).samples().isEmpty());
assertEquals(0, reads.get());
service.publishSnapshots();
IntegrationMetricSnapshot publication = service.snapshotMetrics(keys);
assertEquals(1, reads.get());
assertEquals(sampledAt, publication.samples().get(IntegrationMetricSchema.IRIS_WORLD_COUNT).sampledAtMs());
assertTrue(publication.capturedAtMs() >= sampledAt);
assertEquals(publication, service.snapshotMetrics(keys));
assertEquals(1, reads.get());
}
@Test
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
@@ -0,0 +1,103 @@
package art.arcane.iris.integration;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.junit.Test;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisPapiLifecycleTest {
@Test
public void lateEnableRegistersOnceAndReattachesAfterDisable() throws Exception {
Iris iris = plugin(true);
Plugin placeholders = namedPlugin("PlaceholderAPI");
PluginManager plugins = mock(PluginManager.class);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<HandlerList> handlers = mockStatic(HandlerList.class);
MockedStatic<PlaceholderRegistration> availability = mockStatic(PlaceholderRegistration.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class);
MockedConstruction<PlaceholderRegistration> registrations = mockConstruction(PlaceholderRegistration.class)) {
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
availability.when(PlaceholderRegistration::isPlaceholderApiEnabled).thenReturn(true);
installer.when(() -> IrisPapiInstaller.install(any(), any(), any())).thenReturn(true);
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
assertEquals(1, registrations.constructed().size());
verify(plugins).registerEvents(any(IrisPapiListener.class), eq(iris));
iris.onPlaceholderApiDisable(new PluginDisableEvent(namedPlugin("Unrelated")));
verify(registrations.constructed().getFirst(), never()).unregister();
iris.onPlaceholderApiDisable(new PluginDisableEvent(placeholders));
verify(registrations.constructed().getFirst()).unregister();
handlers.verify(() -> HandlerList.unregisterAll(any(IrisPapiListener.class)));
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
assertEquals(2, registrations.constructed().size());
verify(plugins, times(2)).registerEvents(any(IrisPapiListener.class), eq(iris));
}
}
@Test
public void enableEventsCannotRegisterBeforeIrisIsReadyOrDuringShutdown() throws Exception {
Iris iris = plugin(false);
Plugin placeholders = namedPlugin("PlaceholderAPI");
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
installer.verifyNoInteractions();
}
}
@Test
public void unrelatedEnableEventsDoNotRegisterPlaceholders() throws Exception {
Iris iris = plugin(true);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
iris.onPlaceholderApiEnable(new PluginEnableEvent(namedPlugin("Unrelated")));
installer.verifyNoInteractions();
}
}
private static Iris plugin(boolean active) throws ReflectiveOperationException {
Iris iris = mock(Iris.class);
when(iris.getLogger()).thenReturn(Logger.getAnonymousLogger());
doCallRealMethod().when(iris).onPlaceholderApiEnable(any());
doCallRealMethod().when(iris).onPlaceholderApiDisable(any());
Field lifecycle = Iris.class.getDeclaredField("papiLifecycleActive");
lifecycle.setAccessible(true);
lifecycle.setBoolean(iris, active);
return iris;
}
private static Plugin namedPlugin(String name) {
Plugin plugin = mock(Plugin.class);
when(plugin.getName()).thenReturn(name);
return plugin;
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.update;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.volmlib.util.update.BukkitUpdateService;
import org.bukkit.plugin.Plugin;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
public class IrisUpdateNotifierTest {
@Test
public void acceptedSettingsReconfigureChecksAndClosingRemovesTheListener() {
IrisSettings previous = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
Plugin plugin = mock(Plugin.class);
BukkitUpdateService service = mock(BukkitUpdateService.class);
try (MockedStatic<BukkitUpdateService> registration = mockStatic(BukkitUpdateService.class)) {
registration.when(() -> BukkitUpdateService.register(eq(plugin), any())).thenReturn(service);
IrisUpdateNotifier notifier = new IrisUpdateNotifier(plugin);
try {
ArgumentCaptor<BukkitUpdateService.Options> options = ArgumentCaptor.forClass(BukkitUpdateService.Options.class);
registration.verify(() -> BukkitUpdateService.register(eq(plugin), options.capture()));
assertEquals("VolmitSoftware", options.getValue().owner());
assertEquals("Iris", options.getValue().repository());
assertEquals("iris.update", options.getValue().permission());
assertTrue(options.getValue().enabled().getAsBoolean());
clearInvocations(service);
assertFalse(IrisSettings.applyHotloadSnapshot(
"{\"general\":{\"updateNotifications\":false}}", candidate -> false));
assertTrue(options.getValue().enabled().getAsBoolean());
verifyNoInteractions(service);
IrisSettings.installHotloadSnapshot("{\"general\":{\"updateNotifications\":false}}");
assertFalse(options.getValue().enabled().getAsBoolean());
verify(service).reconfigure();
IrisSettings.settings.getGeneral().setUpdateNotifications(true);
assertFalse(options.getValue().enabled().getAsBoolean());
} finally {
notifier.close();
}
verify(service).close();
clearInvocations(service);
IrisSettings.installHotloadSnapshot("{}");
verifyNoInteractions(service);
} finally {
IrisSettings.settings = previous;
}
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.world.tree;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.junit.Test;
import java.util.Collection;
import java.util.List;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class RoutedBlockBreakEventTest {
@Test
public void nativeLootUsesTheReservedToolCloneWithoutReadingPlayerInventory() {
Block block = mock(Block.class);
Player player = mock(Player.class);
ItemStack reservedTool = mock(ItemStack.class);
ItemStack capturedTool = mock(ItemStack.class);
ItemStack drop = mock(ItemStack.class);
Collection<ItemStack> expected = List.of(drop);
when(reservedTool.clone()).thenReturn(capturedTool);
when(block.getDrops(capturedTool)).thenReturn(expected);
RoutedBlockBreakEvent event = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
block, player, mock(FellingRun.class), mock(TreeFellerSVC.class), reservedTool));
reservedTool.setType(Material.AIR);
assertSame(expected, event.captureNativeDrops());
verify(reservedTool).clone();
verify(block).getDrops(capturedTool);
verify(block, never()).getDrops(reservedTool);
verifyNoInteractions(player);
}
}
+66 -16
View File
@@ -22,6 +22,7 @@ import org.gradle.api.file.FileCopyDetails
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.attributes.Bundling
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
@@ -38,10 +39,14 @@ buildscript {
plugins {
id 'java'
id 'net.fabricmc.fabric-loom' version '1.17.14'
id 'net.fabricmc.fabric-loom' version '1.17.21' apply false
id 'net.fabricmc.fabric-loom-remap' version '1.17.21' apply false
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/mod-target.gradle')
apply plugin: irisModTarget.fabricRemap ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom'
apply plugin: 'art.arcane.volmit-packaging'
interface IrisFileOperations {
@@ -56,9 +61,9 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.1-26.2'))
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.5'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
@@ -141,7 +146,11 @@ configurations.create('jij') {
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
if (irisModTarget.fabricRemap) {
mappings(loom.officialMojangMappings())
}
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(testFixtures('art.arcane:core'))
@@ -168,11 +177,19 @@ dependencies {
libs.fabricApi.keyMapping,
libs.fabricApi.permission
]
if (irisModTarget.fabricModules != null) {
fabricApi = ((Map<String, String>) irisModTarget.fabricModules).collect { String module, String moduleVersion ->
"net.fabricmc.fabric-api:${module}:${moduleVersion}".toString()
}
}
if (irisModTarget.fabricPermissions != null) {
fabricApi.add("me.lucko:fabric-permissions-api:${irisModTarget.fabricPermissions}".toString())
}
fabricApi.each { Object notation ->
add('jij', notation)
add('implementation', notation)
add(modConfiguration, notation)
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly("org.slf4j:slf4j-api:${irisModTarget.slf4j}")
compileOnly(libs.spigot) {
transitive = false
}
@@ -201,21 +218,28 @@ dependencies {
libs.jaxen
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
['bundle', 'devBundle'].each { String configuration ->
add(configuration, notation) {
if (group == 'art.arcane' && name == 'core') {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
}
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
options.release.set((int) irisModTarget.java)
}
tasks.named('test').configure {
}
loom {
accessWidenerPath = art.arcane.volmit.packaging.NativeAccessRules.prepare(nativeSourceImplementation, "fabric", layout.buildDirectory.dir("native-access").get().asFile)
accessWidenerPath = art.arcane.volmit.packaging.NativeAccessRules.prepare(irisModTarget.nativeAccess.toString(), "fabric", layout.buildDirectory.dir("native-access").get().asFile)
runs {
client {
runDir(providers.gradleProperty('irisClientRunDir').getOrElse('run'))
@@ -270,6 +294,15 @@ Map<String, String> nestedFabricApiJars = [
'fabric-permission-api-v1' : 'fabric-permission-api-v1.jar'
]
if (irisModTarget.fabricModules != null) {
nestedFabricApiJars = ((Map<String, String>) irisModTarget.fabricModules).collectEntries { String module, String ignored ->
[(module): "${module}.jar".toString()]
}
}
if (irisModTarget.fabricPermissions != null) {
nestedFabricApiJars.put('fabric-permissions-api', 'fabric-permissions-api.jar')
}
FileTree staleLoaderJars = fileTree(layout.buildDirectory.dir('libs')) {
include('Iris v* [Fabric] *.jar')
include('iris-fabric-*.jar')
@@ -288,6 +321,10 @@ tasks.named('shadowJar', ShadowJar).configure {
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
configurations = [project.configurations.named('bundle').get()]
mergeServiceFiles()
if ((int) irisModTarget.java == 21) {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
@@ -313,6 +350,8 @@ tasks.named('shadowJar', ShadowJar).configure {
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
// loader has. Shipping it only adds dead classes that reference a missing package.
exclude('art/arcane/iris/platform/bukkit/**')
exclude('art/arcane/iris/platform/bootstrap/SlimJar*.class')
exclude('art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries*.class')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
@@ -344,28 +383,39 @@ tasks.named('shadowJar', ShadowJar).configure {
}
}
// The thin `jar`/`remapJar` outputs carry valid fabric.mod.json metadata but bundle zero
// dependencies. Installing one is an instant NoClassDefFoundError, and they sit in build/libs right
// next to the real artifact. shadowJar is the only shippable Fabric jar.
// The thin jar omits bundled dependencies. Obfuscated targets remap the complete shadow jar.
tasks.named('jar').configure {
enabled = false
}
tasks.matching { it.name == 'remapJar' }.configureEach {
enabled = false
enabled = (boolean) irisModTarget.fabricRemap
if (irisModTarget.fabricRemap) {
dependsOn(tasks.named('shadowJar'))
inputFile.set(tasks.named('shadowJar', ShadowJar).flatMap { it.archiveFile })
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
addNestedDependencies.set(false)
}
}
if (irisModTarget.fabricRemap) {
tasks.named('shadowJar', ShadowJar).configure {
destinationDirectory.set(layout.buildDirectory.dir('devlibs'))
}
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
dependsOn(tasks.named(irisModTarget.fabricRemap ? 'remapJar' : 'shadowJar'))
}
pluginPackaging {
artifacts {
create('distribution') {
taskName = 'shadowJar'
taskName = irisModTarget.fabricRemap ? 'remapJar' : 'shadowJar'
policyName = 'Iris Fabric'
prunePrefixes = ['art/arcane/volmlib/']
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
}
}
}
apply from: file('../../gradle/modded-version-sources.gradle')
+2
View File
@@ -33,6 +33,8 @@ plugins {
rootProject.name = 'iris-fabric'
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
dependencyResolutionManagement {
versionCatalogs {
libs {
@@ -0,0 +1,78 @@
{
"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)"
],
"contributors": [
"cyberpwn",
"NextdoorPsycho",
"Vatuu"
],
"contact": {
"homepage": "https://docs.volmit.com/iris/",
"issues": "https://github.com/VolmitSoftware/Iris/issues",
"sources": "https://github.com/VolmitSoftware/Iris"
},
"license": "GPL-3.0",
"icon": "assets/irisworldgen/icon.png",
"environment": "*",
"accessWidener": "volmlib.accesswidener",
"mixins": [
"volmlib.fabric.mixins.json",
"volmlib.entity.mixins.json",
{
"config": "volmlib.client.mixins.json",
"environment": "client"
}
],
"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-v0.jar"
},
{
"file": "META-INF/jars/fabric-lifecycle-events-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-networking-api-v1.jar"
},
{
"file": "META-INF/jars/fabric-rendering-v1.jar"
},
{
"file": "META-INF/jars/fabric-key-binding-api-v1.jar"
},
{
"file": "META-INF/jars/fabric-permissions-api.jar"
}
],
"depends": {
"fabricloader": ">=0.19.5",
"minecraft": "${minecraftVersion}",
"java": ">=21",
"fabric-permissions-api-v0": "*"
}
}
@@ -0,0 +1,102 @@
package art.arcane.volmlib.nativelib.minecraft26_2.fabric;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeRegistryDefinitions;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class NativeFabricBiomeDefinitionTest {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String BIOME_KEY = "iris:biomes/fabric_modified";
private static final String SOURCE_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"fog_color\":12638463,\"water_color\":4159204,\"water_fog_color\":329011,\"sky_color\":7907327},"
+ "\"carvers\":{},\"features\":[],\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generatedBiomeDefinitionExcludesFabricBiomeModifications() throws Exception {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
Holder.Reference<PlacedFeature> ore = features.register(
ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.parse("iris:modded_ore")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()),
RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, features, carvers));
Biome source = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(SOURCE_JSON)).getOrThrow();
NativeFabricLoader loader = new NativeFabricLoader(new NativeLoaderOptions("irisworldgen", "treefeller",
BooleanSupplier::getAsBoolean));
loader.recordOriginalBiome(source);
Holder.Reference<Biome> biome = biomes.register(
ResourceKey.create(Registries.BIOME, ResourceLocation.parse(BIOME_KEY)), source, RegistrationInfo.BUILT_IN);
biomes.freeze();
addFeatureInPlace(biome.value().getGenerationSettings(), ore);
warmInPlace(biome.value());
NativeRegistryDefinitions definitions = new NativeRegistryDefinitions(() -> access, loader::unmodifiedBiome);
assertEquals(0.1F, biome.value().getBaseTemperature(), 0.0F);
assertTrue(biome.value().getGenerationSettings().features()
.get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal()).contains(ore));
assertEquals(JsonParser.parseString(definitions.canonicalDefinition(BIOME_REGISTRY, BIOME_KEY, SOURCE_JSON).value()),
JsonParser.parseString(definitions.generatedDefinition(BIOME_REGISTRY, BIOME_KEY).value()));
}
private static void addFeatureInPlace(BiomeGenerationSettings settings, Holder<PlacedFeature> feature)
throws ReflectiveOperationException {
List<HolderSet<PlacedFeature>> steps = new ArrayList<>(settings.features());
while (steps.size() <= GenerationStep.Decoration.UNDERGROUND_ORES.ordinal()) {
steps.add(HolderSet.direct());
}
steps.set(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal(), HolderSet.direct(feature));
Field features = BiomeGenerationSettings.class.getDeclaredField("features");
features.setAccessible(true);
features.set(settings, List.copyOf(steps));
}
private static void warmInPlace(Biome biome) throws ReflectiveOperationException {
Constructor<Biome.ClimateSettings> climate = Biome.ClimateSettings.class.getDeclaredConstructor(
boolean.class, float.class, Biome.TemperatureModifier.class, float.class);
climate.setAccessible(true);
Field climateSettings = Biome.class.getDeclaredField("climateSettings");
climateSettings.setAccessible(true);
climateSettings.set(biome, climate.newInstance(true, 0.1F, Biome.TemperatureModifier.NONE, 0.4F));
}
}
+46 -10
View File
@@ -17,11 +17,13 @@
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import net.minecraftforge.gradle.shadow.net.minecraftforge.gradleutils.shared.EnhancedPlugin
import org.gradle.api.file.DeleteSpec
import org.gradle.api.file.FileCopyDetails
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.attributes.Bundling
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
@@ -42,6 +44,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/mod-target.gradle')
apply plugin: 'art.arcane.volmit-packaging'
interface IrisFileOperations {
@@ -56,9 +60,20 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.1-26.2'))
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.3-66.0.8'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
? gradle.extensions.extraProperties.get('irisBuildLane').toString() : null
if (buildLane != null) {
File laneCache = providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()
? new File(gradle.gradleUserHomeDir, "caches/iris-forge/${buildLane}")
: file(".gradle/iris-lanes/${buildLane}")
plugins.withType(EnhancedPlugin).configureEach { EnhancedPlugin<?> forgePlugin ->
forgePlugin.globalCaches().set(new File(laneCache, 'forgegradle'))
}
minecraft.mavenizerOutput.set(new File(laneCache, 'mavenizer/repo'))
}
Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) {
@@ -158,14 +173,16 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
}
dependencies {
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
implementation(buildLane == null
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(testFixtures('art.arcane:core'))
testImplementation(libs.spigot) {
transitive = false
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly("org.slf4j:slf4j-api:${irisModTarget.slf4j}")
compileOnly(libs.spigot) {
transitive = false
}
@@ -200,14 +217,23 @@ dependencies {
libs.jaxen
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
add('runBundle', notation)
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
add(configuration, notation) {
if (group == 'art.arcane' && name == 'core') {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
}
}
}
minecraft {
accessTransformer.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(nativeSourceImplementation, "forge", layout.buildDirectory.dir("native-access").get().asFile))
if (irisModTarget.fabricRemap) {
mappings channel: 'official', version: minecraftVersion
}
accessTransformer.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(irisModTarget.nativeAccess.toString(), "forge", layout.buildDirectory.dir("native-access").get().asFile))
runs {
register('client') {
workingDir = layout.projectDirectory.dir('run')
@@ -241,7 +267,8 @@ minecraft {
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
options.compilerArgs.addAll(['-Xmaxerrs', '2000'])
options.release.set((int) irisModTarget.java)
}
tasks.named('test').configure {
@@ -250,8 +277,9 @@ tasks.named('test').configure {
processResources {
inputs.property('version', irisVersion)
inputs.property('minecraftVersion', minecraftVersion)
inputs.property('loaderRange', irisModTarget.forgeRange.toString())
filesMatching('META-INF/mods.toml') {
expand('version': irisVersion, 'minecraftVersion': minecraftVersion)
expand('version': irisVersion, 'minecraftVersion': minecraftVersion, 'loaderRange': irisModTarget.forgeRange.toString())
}
}
@@ -280,6 +308,10 @@ tasks.named('shadowJar', ShadowJar).configure {
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
if ((int) irisModTarget.java == 21) {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
@@ -305,6 +337,8 @@ tasks.named('shadowJar', ShadowJar).configure {
// The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod
// loader has. Shipping it only adds dead classes that reference a missing package.
exclude('art/arcane/iris/platform/bukkit/**')
exclude('art/arcane/iris/platform/bootstrap/SlimJar*.class')
exclude('art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries*.class')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
filesMatching('META-INF/services/**') { FileCopyDetails entry ->
entry.duplicatesStrategy = DuplicatesStrategy.INCLUDE
@@ -343,3 +377,5 @@ pluginPackaging {
}
}
}
apply from: file('../../gradle/modded-version-sources.gradle')
+2
View File
@@ -33,6 +33,8 @@ plugins {
rootProject.name = 'iris-forge'
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
dependencyResolutionManagement {
versionCatalogs {
libs {
@@ -1,5 +1,5 @@
modLoader = "javafml"
loaderVersion = "[66,67)"
loaderVersion = "${loaderRange}"
license = "GPL-3.0"
issueTrackerURL = "https://github.com/VolmitSoftware/Iris/issues"
@@ -16,7 +16,7 @@ logoFile = "assets/irisworldgen/icon.png"
[[dependencies.irisworldgen]]
modId = "forge"
mandatory = true
versionRange = "[66,67)"
versionRange = "${loaderRange}"
ordering = "NONE"
side = "BOTH"
@@ -0,0 +1,29 @@
package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLifecycleCallbacks;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkGeneratorDefinition;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeLoader;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeBootstrap;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
@Mod("irisworldgen")
public final class IrisForgeBootstrap {
public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
NativeForgeLoader loader = new NativeForgeLoader(new NativeLoaderOptions(
"irisworldgen", "treefeller", ModdedTreeFellerService::runBreakProbe));
NativeChunkGeneratorDefinition generator = IrisModdedChunkGenerator.DEFINITION;
ModdedEngineBootstrap.bootCommon(loader, "Forge " + FMLLoader.versionInfo().forgeVersion(),
() -> NativeForgeBootstrap.registerGenerators(context, generator, "block_drops"));
ForgeProtocolNetworking.register();
if (loader.clientEnvironment()) {
IrisForgeClient.init(context);
}
NativeForgeBootstrap.install(context, loader, ModdedLifecycleCallbacks.create());
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.forge;
import art.arcane.iris.client.IrisClient;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeClientHooks;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolChannel;
public final class IrisForgeClient {
private IrisForgeClient() {
}
public static void init(FMLJavaModLoadingContext context) {
IrisClient.bindSender(IrisForgeClient::sendToServer);
NativeForgeClientHooks.install(context, IrisClientHud.binding(), IrisClient::onWorldJoin, IrisClient::onDisconnect);
}
private static void sendToServer(byte[] frame) {
NativeProtocolChannel channel = ForgeProtocolNetworking.channel();
if (channel == null) {
return;
}
channel.sendToServer(frame);
}
}
@@ -0,0 +1,100 @@
package art.arcane.volmlib.nativelib.minecraft26_2.forge;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeRegistryDefinitions;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraftforge.common.world.BiomeModifier;
import net.minecraftforge.common.world.ModifiableBiomeInfo;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class NativeForgeBiomeDefinitionTest {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String BIOME_KEY = "iris:biomes/forge_modified";
private static final String SOURCE_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"fog_color\":12638463,\"water_color\":4159204,\"water_fog_color\":329011,\"sky_color\":7907327},"
+ "\"carvers\":{},\"features\":[],\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generatedBiomeDefinitionExcludesForgeBiomeModifierOutput() {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
Holder.Reference<PlacedFeature> ore = features.register(
ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.parse("iris:modded_ore")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()),
RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, features, carvers));
Biome source = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(SOURCE_JSON)).getOrThrow();
Holder.Reference<Biome> biome = biomes.register(
ResourceKey.create(Registries.BIOME, ResourceLocation.parse(BIOME_KEY)), source, RegistrationInfo.BUILT_IN);
biomes.freeze();
biome.value().modifiableBiomeInfo().applyBiomeModifiers(biome, List.of(new ModdedContent(ore)));
NativeRegistryDefinitions definitions = new NativeRegistryDefinitions(() -> access,
new NativeForgeLoader(new NativeLoaderOptions("irisworldgen", "treefeller",
BooleanSupplier::getAsBoolean))::unmodifiedBiome);
assertEquals(0.1F, biome.value().getBaseTemperature(), 0.0F);
assertEquals(0x102030, biome.value().getWaterColor() & 0xFFFFFF);
assertTrue(biome.value().getGenerationSettings().hasFeature(ore.value()));
assertTrue(biome.value().getMobSettings().getMobs(MobCategory.MONSTER).unwrap().stream()
.anyMatch(spawn -> spawn.type == EntityType.ZOMBIE));
assertEquals(JsonParser.parseString(definitions.canonicalDefinition(BIOME_REGISTRY, BIOME_KEY, SOURCE_JSON).value()),
JsonParser.parseString(definitions.generatedDefinition(BIOME_REGISTRY, BIOME_KEY).value()));
}
private record ModdedContent(Holder<PlacedFeature> ore) implements BiomeModifier {
@Override
public void modify(Holder<Biome> biome, Phase phase, ModifiableBiomeInfo.BiomeInfo.Builder builder) {
if (phase != Phase.ADD) {
return;
}
builder.getClimateSettings().setTemperature(0.1F);
builder.getSpecialEffects().waterColor(0x102030);
builder.getMobSpawnSettings().addSpawn(MobCategory.MONSTER, new MobSpawnSettings.SpawnerData(EntityType.ZOMBIE, 5, 1, 2));
builder.getGenerationSettings().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, ore);
}
@Override
public MapCodec<? extends BiomeModifier> codec() {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,54 @@
package art.arcane.volmlib.nativelib.minecraft26_2.forge;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.MobSpawnEvent;
import net.minecraftforge.eventbus.api.Event;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeForgeSpawnPositionTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void chunkGenerationSpawnsFireThePositionCheckEventAndHonorItsDenial() {
NativeForgeLoader loader = new NativeForgeLoader(
new NativeLoaderOptions("irisworldgen", "treefeller", BooleanSupplier::getAsBoolean));
ServerLevelAccessor level = mock(ServerLevelAccessor.class);
Mob mob = mock(Mob.class);
when(mob.checkSpawnRules(level, MobSpawnType.CHUNK_GENERATION)).thenReturn(true);
when(mob.checkSpawnObstruction(level)).thenReturn(true);
AtomicReference<MobSpawnType> observed = new AtomicReference<>();
Consumer<MobSpawnEvent.PositionCheck> listener = event -> {
observed.set(event.getSpawnType());
event.setResult(Event.Result.DENY);
};
MinecraftForge.EVENT_BUS.addListener(listener);
try {
assertFalse(loader.checkSpawnPosition(mob, level, MobSpawnType.CHUNK_GENERATION));
} finally {
MinecraftForge.EVENT_BUS.unregister(listener);
}
assertEquals(MobSpawnType.CHUNK_GENERATION, observed.get());
assertTrue(loader.checkSpawnPosition(mob, level, MobSpawnType.CHUNK_GENERATION));
}
}
@@ -0,0 +1,101 @@
package art.arcane.volmlib.nativelib.minecraft26_2.forge;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeRegistryDefinitions;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraftforge.common.world.BiomeModifier;
import net.minecraftforge.common.world.ModifiableBiomeInfo;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class NativeForgeBiomeDefinitionTest {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String BIOME_KEY = "iris:biomes/forge_modified";
private static final String SOURCE_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"water_color\":\"#3f76e4\"},\"carvers\":[],\"features\":[],"
+ "\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generatedBiomeDefinitionExcludesForgeBiomeModifierOutput() {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
Holder.Reference<PlacedFeature> ore = features.register(
ResourceKey.create(Registries.PLACED_FEATURE, Identifier.parse("iris:modded_ore")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()),
RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, features, carvers));
Biome source = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(SOURCE_JSON)).getOrThrow();
Holder.Reference<Biome> biome = biomes.register(
ResourceKey.create(Registries.BIOME, Identifier.parse(BIOME_KEY)), source, RegistrationInfo.BUILT_IN);
biomes.freeze();
biome.value().modifiableBiomeInfo().applyBiomeModifiers(biome, List.of(new ModdedContent(ore)));
NativeRegistryDefinitions definitions = new NativeRegistryDefinitions(() -> access,
new NativeForgeLoader(new NativeLoaderOptions("irisworldgen", "treefeller",
BooleanSupplier::getAsBoolean))::unmodifiedBiome);
assertEquals(0.1F, biome.value().modifiableBiomeInfo().get().climateSettings().temperature(), 0.0F);
assertEquals(0x102030, biome.value().modifiableBiomeInfo().get().effects().waterColor() & 0xFFFFFF);
assertTrue(biome.value().getGenerationSettings().hasFeature(ore.value()));
assertTrue(biome.value().getMobSettings().getMobs(MobCategory.MONSTER).unwrap().stream()
.anyMatch(spawn -> spawn.value().type() == EntityTypes.ZOMBIE));
assertEquals(JsonParser.parseString(definitions.canonicalDefinition(BIOME_REGISTRY, BIOME_KEY, SOURCE_JSON).value()),
JsonParser.parseString(definitions.generatedDefinition(BIOME_REGISTRY, BIOME_KEY).value()));
}
private record ModdedContent(Holder<PlacedFeature> ore) implements BiomeModifier {
@Override
public void modify(Holder<Biome> biome, Phase phase, ModifiableBiomeInfo.BiomeInfo.Builder builder) {
if (phase != Phase.ADD) {
return;
}
builder.getClimateSettings().setTemperature(0.1F);
builder.getSpecialEffects().waterColor(0x102030);
builder.getMobSpawnSettings().addSpawn(MobCategory.MONSTER, 5,
new MobSpawnSettings.SpawnerData(EntityTypes.ZOMBIE, 1, 2));
builder.getGenerationSettings().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, ore);
}
@Override
public MapCodec<? extends BiomeModifier> codec() {
throw new UnsupportedOperationException();
}
}
}
@@ -57,6 +57,8 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenera
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorContext;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
@@ -100,6 +102,7 @@ import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
@@ -477,6 +480,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
return existing;
}
try {
applyReplacementSeed(level);
Engine created = ModdedWorldEngines.get(
level,
activePack,
@@ -511,6 +515,16 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
}
}
private void applyReplacementSeed(NativeWorld level) {
if (seedOverride != Long.MIN_VALUE) {
return;
}
OptionalLong replacementSeed = ModdedDimensionReplacements.loadedSeed(level.name());
if (replacementSeed.isPresent()) {
seedOverride = replacementSeed.getAsLong();
}
}
private void recordBindFailure(NativeWorld level, Throwable error) {
// A runtime that is stopping says nothing about the world: fail this call, let the next bind try again.
if (GenerationFailures.isShutdownFailure(error)) {
@@ -743,14 +757,8 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
PackValidationRegistry.requireLoadable(packRoot);
packDirectory = packRoot.toFile();
}
IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
+ "' missing from pack " + packDirectory.getAbsolutePath());
}
ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension));
ModdedDimensionMetadata.ConfiguredPack resolved =
ModdedDimensionMetadata.ConfiguredPack.load(packDirectory, activeDimensionKey);
configuredPack = resolved;
heightMetadata = resolved.metadata();
return resolved;
@@ -823,12 +831,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
importedFeatures.invalidate();
}
public void updateRegeneratedChunk(int chunkX, int chunkZ) {
public void updateRegeneratedChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
ModdedChunkUpdateService service = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
if (service == null) {
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
}
service.updateRegeneratedChunk(engine(), boundLevel(), chunkX, chunkZ);
service.updateRegeneratedChunk(engine(), boundLevel(), mantle, chunkX, chunkZ);
}
public NativeBiomeResolver regenBiomeResolver() {
@@ -239,50 +239,21 @@ public final class MainWorldService {
private static Path quarantineVanillaDimensions(Path worldRoot) throws IOException {
Path recovery = markerFile().getParent().resolve("mainworld-recovery-" + UUID.randomUUID());
List<Path> moved = new ArrayList<>();
List<Path> sources = new ArrayList<>();
for (String file : NativeServerWorldLayout.metadataFiles()) {
moveToRecovery(worldRoot, worldRoot.resolve(file), recovery, moved);
sources.add(worldRoot.resolve(file));
}
for (String folder : NativeServerWorldLayout.primaryFolders()) {
moveToRecovery(worldRoot, worldRoot.resolve(folder), recovery, moved);
sources.add(worldRoot.resolve(folder));
}
moveToRecovery(worldRoot, worldRoot.resolve(IrisEngineMantle.STORAGE_FOLDER_NAME), recovery, moved);
sources.add(worldRoot.resolve(IrisEngineMantle.STORAGE_FOLDER_NAME));
for (String folder : NativeServerWorldLayout.dimensionFolders()) {
moveToRecovery(worldRoot, worldRoot.resolve(folder), recovery, moved);
}
if (moved.isEmpty()) {
Files.deleteIfExists(recovery);
sources.add(worldRoot.resolve(folder));
}
ModdedWorldQuarantine.move(worldRoot, sources, recovery);
return recovery;
}
private static void moveToRecovery(Path worldRoot, Path source, Path recovery,
List<Path> moved) throws IOException {
if (!Files.exists(source)) {
return;
}
Path relative = worldRoot.relativize(source);
Path target = recovery.resolve(relative);
Files.createDirectories(target.getParent());
try {
Files.move(source, target);
moved.add(relative);
} catch (IOException failure) {
for (int index = moved.size() - 1; index >= 0; index--) {
Path rollbackRelative = moved.get(index);
Path rollbackSource = recovery.resolve(rollbackRelative);
Path rollbackTarget = worldRoot.resolve(rollbackRelative);
try {
Files.createDirectories(rollbackTarget.getParent());
Files.move(rollbackSource, rollbackTarget);
} catch (IOException rollbackFailure) {
failure.addSuppressed(rollbackFailure);
}
}
throw failure;
}
}
private static String escape(String value) {
return value.replace(":", "\\:");
}
@@ -0,0 +1,38 @@
/*
* 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;
public final class MissingPackDimensionException extends IllegalStateException {
private final String pack;
private final String dimensionKey;
public MissingPackDimensionException(String pack, String dimensionKey) {
super("Pack '" + pack + "' does not contain dimensions/" + dimensionKey + ".json");
this.pack = pack;
this.dimensionKey = dimensionKey;
}
public String pack() {
return pack;
}
public String dimensionKey() {
return dimensionKey;
}
}
@@ -42,6 +42,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.locator.BiomeSearchFilter;
import art.arcane.volmlib.util.math.RNG;
import java.io.IOException;
@@ -300,10 +301,6 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome search has no active engine runtime");
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return query.defaultFallback().get();
}
Set<H> candidates = new HashSet<>();
for (H biome : collectVisibleBiomeHolders()) {
if (query.allowed().test(biome)) {
@@ -313,16 +310,86 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
if (candidates.isEmpty()) {
return null;
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext != null) {
return query.search().search(candidates, (blockX, blockZ) -> {
int quartX = blockX >> 2;
int quartZ = blockZ >> 2;
DimensionStackLayout stackLayout = stackContext.sample(quartX << 2, quartZ << 2);
return blockY -> resolveVisibleBiome(engine, quartX, blockY >> 2, quartZ,
query.sampler(), stackLayout);
});
}
BiomeSearchFilter filter = BiomeSearchFilter.of(engine, searchBiomeKeys(engine, candidates));
int minHeight = engine.getMinHeight();
return query.search().search(candidates, (blockX, blockZ) -> {
int quartX = blockX >> 2;
int quartZ = blockZ >> 2;
DimensionStackLayout stackLayout = stackContext.sample(quartX << 2, quartZ << 2);
return blockY -> resolveVisibleBiome(engine, quartX, blockY >> 2, quartZ,
query.sampler(), stackLayout);
BiomeSearchFilter.Column column = filter.column(quartX << 2, quartZ << 2);
return blockY -> column.admits(((blockY >> 2) << 2) - minHeight)
? locateVisibleBiome(engine, quartX, blockY >> 2, quartZ, query.sampler())
: null;
});
}
}
private H locateVisibleBiome(Engine engine, int quartX, int quartY, int quartZ, S sampler) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openHistoryCoordinateScope(
engine, quartX << 2, quartZ << 2, "modded_locate_visible_biome")) {
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
}
}
private Set<String> searchBiomeKeys(Engine engine, Set<H> candidates) {
NativeBiomeSourceAccess.RegistryView<H> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris visible biome search has no biome registry");
}
H plains = resolveHolder(registry, "minecraft:plains");
boolean fallbackCandidate = plains != null && candidates.contains(plains);
Set<String> keys = new HashSet<>();
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
if (emitsCandidate(engine, ownedBiome, registry, candidates, fallbackCandidate)) {
keys.add(ownedBiome.biome().getLoadKey());
}
}
return keys;
}
private boolean emitsCandidate(Engine engine, OwnedBiome ownedBiome, NativeBiomeSourceAccess.RegistryView<H> registry,
Set<H> candidates, boolean fallbackCandidate) {
IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
if (customBiome == null) {
if (fallbackCandidate) {
return true;
}
continue;
}
String key = ownedBiome.dimension() == engine.getDimension() && ownedBiome.data() == engine.getData()
? ModdedWorldgenIds.biomeRef(engine, customBiome.getId())
: ownedBiome.data().customBiomeResourceKey(ownedBiome.dimension(), customBiome);
if (emits(registry, key, candidates, fallbackCandidate)) {
return true;
}
}
return false;
}
for (String key : irisBiome.getVisibleBiomeKeys(engine)) {
if (emits(registry, key, candidates, fallbackCandidate)) {
return true;
}
}
return false;
}
private static <H> boolean emits(NativeBiomeSourceAccess.RegistryView<H> registry, String key,
Set<H> candidates, boolean fallbackCandidate) {
H holder = key == null ? null : resolveHolder(registry, key);
return holder == null ? fallbackCandidate : candidates.contains(holder);
}
private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
S sampler) {
if (isGuaranteedSurfaceBiome(engine, quartY)) {
@@ -31,6 +31,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockDropHooks;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.structure.placement.LootResolver;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -181,26 +182,30 @@ public final class ModdedBlockBreakHandler {
NativeBlockState brokenState,
Engine engine
) {
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
if (providers.isEmpty()) {
return Result.empty();
}
BiomeEnvironment environment = engine.getBiomeEnvironment(position.x(),
position.y() - engine.getMinHeight(), position.z());
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
KList<IrisBlockDrops> providers = providers(environment, brokenState);
if (providers.isEmpty()) {
return Result.empty();
}
KList<NativeItemStack> drops = new KList<>();
boolean replaceVanillaDrops = false;
for (IrisBlockDrops provider : providers) {
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
for (IrisLoot loot : provider.getDrops()) {
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
continue;
}
NativeItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, ModdedItemTranslator.context(level));
if (stack != null && !stack.isEmpty()) {
drops.add(stack);
KList<NativeItemStack> drops = new KList<>();
boolean replaceVanillaDrops = false;
for (IrisBlockDrops provider : providers) {
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
for (IrisLoot loot : provider.getDrops()) {
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
continue;
}
NativeItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, ModdedItemTranslator.context(level));
if (stack != null && !stack.isEmpty()) {
drops.add(stack);
}
}
}
return new Result(drops, replaceVanillaDrops, null);
}
return new Result(drops, replaceVanillaDrops, null);
}
private static void removeMarker(Engine engine, NativeBlockPoint position) {
@@ -225,22 +230,17 @@ public final class ModdedBlockBreakHandler {
engine.getMantle().getMantle().remove(position.x(), mantleY, position.z(), TreeBlockMaterial.class);
}
private static KList<IrisBlockDrops> providers(Engine engine, NativeBlockPoint position, NativeBlockState brokenState) {
private static KList<IrisBlockDrops> providers(BiomeEnvironment environment, NativeBlockState brokenState) {
KList<IrisBlockDrops> providers = new KList<>();
IrisData data = engine.getData();
int relativeY = position.y() - engine.getMinHeight();
IrisBiome biome = engine.getBiome(position.x(), relativeY, position.z());
if (biome != null) {
addMatching(providers, biome.getBlockDrops(), brokenState, data);
}
IrisData data = environment.data();
IrisBiome biome = environment.biome();
addMatching(providers, biome.getBlockDrops(), brokenState, data);
if (skipsParents(providers)) {
return providers;
}
IrisRegion region = engine.getRegion(position.x(), relativeY, position.z());
if (region != null) {
addMatching(providers, region.getBlockDrops(), brokenState, data);
}
addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data);
IrisRegion region = environment.region();
addMatching(providers, region.getBlockDrops(), brokenState, data);
addMatching(providers, environment.dimension().getBlockDrops(), brokenState, data);
return providers;
}
@@ -0,0 +1,92 @@
/*
* 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 java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
final class ModdedDatapackTagMerge {
private static final String REPLACE = "replace";
private static final String VALUES = "values";
private static final String REMOVE = "remove";
private ModdedDatapackTagMerge() {
}
static boolean isTag(Path relative) {
return relative.getNameCount() >= 4
&& "data".equals(relative.getName(0).toString())
&& "tags".equals(relative.getName(2).toString())
&& relative.getFileName().toString().endsWith(".json");
}
static void merge(Path source, Path destination) throws IOException {
JSONObject incoming = read(source);
JSONObject existing = read(destination);
JSONObject tag = new JSONObject();
tag.put(REPLACE, existing.optBoolean(REPLACE, false) || incoming.optBoolean(REPLACE, false));
tag.put(VALUES, union(existing, incoming, VALUES, source, destination));
if (existing.has(REMOVE) || incoming.has(REMOVE)) {
tag.put(REMOVE, union(existing, incoming, REMOVE, source, destination));
}
Files.writeString(destination, tag.toString(4), StandardCharsets.UTF_8);
}
private static JSONArray union(JSONObject existing, JSONObject incoming, String field, Path source, Path destination)
throws IOException {
Map<String, Object> entries = new LinkedHashMap<>();
collect(existing, field, entries, destination);
collect(incoming, field, entries, source);
JSONArray merged = new JSONArray();
for (Object entry : entries.values()) {
merged.put(entry);
}
return merged;
}
private static JSONObject read(Path tag) throws IOException {
try {
return new JSONObject(Files.readString(tag, StandardCharsets.UTF_8));
} catch (RuntimeException invalid) {
throw new IOException("Iris cannot merge the invalid datapack tag " + tag + ".", invalid);
}
}
private static void collect(JSONObject tag, String field, Map<String, Object> entries, Path origin) throws IOException {
JSONArray values = tag.optJSONArray(field);
if (values == null) {
return;
}
for (int index = 0; index < values.length(); index++) {
Object entry = values.get(index);
String key = entry instanceof JSONObject object ? object.optString("id", "") : entry.toString();
if (key.isBlank()) {
throw new IOException("Iris cannot merge datapack tag " + origin + ": " + field + " entry " + index + " has no id.");
}
entries.putIfAbsent(key, entry);
}
}
}
Loaded 100 of 543 files, more files were not shown because too many files have changed in this diff. Show more