Compare commits

..
Author SHA1 Message Date
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 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
Brian Neumann-Fopiano cfa274825f Refine terrain and pregeneration
- Add coherent river width variation and shape source and terminal pools.
- Preserve separated hydrology intervals when checking river connectivity.
- Reduce footprint sorting, engine context lookup, and terrain blending work.
- Reuse modded snapshot validation after content and registry checks.
- Explain production pack update staging and restart requirements.
- Retain pregeneration map updates before its desktop window opens.
- Improve pregeneration layout, coordinate hover, keyboard controls, and error details.
- Add the new desktop labels to every supported language catalog.
- Add the buildPsychoLT entry point for verified platform outputs.
- Cover terrain, validation, and desktop behavior with regression tests.
2026-10-07 10:25:41 -04:00
Brian Neumann-Fopiano e72189adc7 Adopt cooperative reload cleanup
- Return cleanup completion from the committed Iris reload callback
2026-10-07 10:24:59 -04:00
Brian Neumann-Fopiano 7f5ce70f49 Improve generation and lifecycle safety
- Reduce terrain, noise, hydrology, and decorator sampling overhead.
- Reduce object transaction allocations while preserving rollback behavior.
- Optimize structure placement and cave anchor sampling.
- Parallelize Studio cache warmup and protect world entry and hotload recovery.
- Validate pack fingerprints and publish pack replacements atomically.
- Notify modded operators about outdated packs without synchronizing pack data.
- Harden world creation, loading, evacuation, and teleport failure recovery.
- Coalesce queued region flushes while preserving chunk save ordering.
- Remove obsolete mantle, pack, and cave planning code.
- Add regression tests and reusable gameplay fixtures.
2026-10-06 20:51:28 -04:00
Brian Neumann-Fopiano a2bcd7a302 V+
- Bump Iris from 4.3.0-26.2 to 4.3.1-26.2.
- Align Fabric, Forge, and NeoForge version defaults with the release.
2026-10-05 16:48:32 -04:00
Brian Neumann-Fopiano 7226fc4400 Refine organic cave generation
- Add seeded cave surface variation and rounded basin undersides.
- Keep dry cave intersections open while preserving fluid seals.
- Apply room biome decorators and preserve their floor substrates.
- Allow ceiling growth above reserved walking clearance.
- Restrict feature placement by region across live and saved generation.
- Cover cave geometry, placement, decoration, and regional filtering with regression tests.
2026-10-05 16:22:55 -04:00
Brian Neumann-Fopiano c479e94c49 Fix biome names and conditions
- Register readable client biome IDs while preserving saved registry identities.
- Audit the modded registry naming mixin.
- Return false from MythicMobs conditions while saved biome data loads.
- Cover biome naming and MythicMobs loading behavior with regression tests.
2026-10-04 20:10:26 -04:00
Brian Neumann-Fopiano ee5aaec87c Fixed Structure Crash
- Don't carash anymore when you disable structures in the server.properties.
2026-10-04 15:32:54 -04:00
Brian Neumann-Fopiano 65ae3f1b45 Cleanup 2026-10-04 15:17:12 -04:00
Brian Neumann-Fopiano a6149b27c9 Adapter patches
(tests)
2026-10-04 13:08:41 -04:00
Brian Neumann-Fopiano 4b4b418c61 Merge Iris generation changes
- Combine authored subterrain features with native CPU generation.
- Verify dimension JSON and schemas preserve subterrain configuration.
2026-10-04 10:11:44 -04:00
Brian Neumann-Fopiano 8d5cf43139 Enable native noise generation
- Enable exact Rust Billow sampling and report the selected backend.
- Add generation parity probes and native comparison tooling.
2026-10-04 09:59:58 -04:00
Brian Neumann-Fopiano 5858eb366d Add subterrain features
- Generate authored underground rooms, materials, fluids, and formations.
- Preserve underground biome ownership across generation and saved terrain.
- Add subterrain searches and regression coverage.
2026-10-04 09:59:57 -04:00
Brian Neumann-Fopiano be0d3b0156 Fix seeded entity spawning
- Derive initial chunk and marker populations from stable location seeds.
- Persist initial spawn completion after region-owned application.
- Seed entity loot and attribute modifier identities.
- Cover spawning, reload completion, and registry fixtures with regression tests.
2026-10-04 06:14:46 -04:00
Brian Neumann-Fopiano 12e8bb5a09 v 2026-10-02 17:13:30 -04:00
Brian Neumann-Fopiano a765e76aa3 Restore natural mob spawning
- Restore cave spawning and correct floor placement across platforms.
- Enforce category population limits and clamp ambient spawn batches.
- Honor mob-spawning rules and entity-ticking chunk admission.
- Separate chunk saving from configurable distance-despawn retention.
- Add spawning regressions and Folia gameplay acceptance coverage.
2026-10-02 16:55:52 -04:00
Brian Neumann-Fopiano 4ef6fc7cc6 Enforce seed cohesion
- Scope cached generation fields to their seed and context.
- Warm decorators and ores with their canonical generation seeds.
- Stabilize object traversal and procedural placement choices.
- Isolate smart-bore variants from shared source objects.
- Pin noise and full-engine output across generation orders.
- Verify same-jar generation through cold server restarts.
2026-10-02 14:43:48 -04:00
Brian Fopiano 6b82ea99e2 Isolate scaled noise generators
- Isolate biome-child and carving-child noise generators.
- Isolate decorator variance generators.
- Share fresh scaled-generator construction.
- Test shared-generator isolation at each caller.
2026-10-02 07:48:01 -04:00
Brian Fopiano 813f3c7bcf Fix continent zoom compounding
- Build independent continent generators before applying zoom.
- Share continent construction across runtime contexts.
- Cover repeated construction and shared-generator isolation.
2026-10-02 07:47:31 -04:00
CeeVeeR 318c1e100b chore:
- Extracted shared style generator sampling to GeneratorSampling.sampleSharedGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:37:51 +02:00
CeeVeeR 357f14e900 chore:
- Extracted shared SAMPLE_BLOCKS to GeneratorSampling

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:36:55 +02:00
CeeVeeR 99cc2383d6 chore:
- Replaced IrisBiome children generator scaling with createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:35:22 +02:00
CeeVeeR 76d71f8344 chore:
- Replaced IrisDimensionCarvingEntry shrink scaling with createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:34:21 +02:00
CeeVeeR 1544c34f44 chore:
- Moved continent zoom scaling to IrisGeneratorStyle.createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:33:17 +02:00
CeeVeeR 00601678d9 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisDecorator
- Removed @ExpectedToFailUntilFixed from the IrisDecorator test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:17:57 +02:00
CeeVeeR 700dc53559 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisBiome
- Removed @ExpectedToFailUntilFixed from the IrisBiome test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:17:19 +02:00
CeeVeeR 0faae93049 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisDimensionCarvingEntry
- Removed @ExpectedToFailUntilFixed from the IrisDimensionCarvingEntry test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:16:41 +02:00
CeeVeeR 92310ef839 test:
- Added decorator shared variance style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:15:33 +02:00
CeeVeeR 5fa6d82b79 chore:
- Extracted first variance generator build to buildVarianceGeneratorOnce

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:14:58 +02:00
CeeVeeR 2078bece40 chore:
- Extracted variance generator construction to createVarianceGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:14:23 +02:00
CeeVeeR 077b48f47f chore:
- Extracted variance seed derivation to deriveVarianceSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:13:48 +02:00
CeeVeeR 7981fa3ce5 test:
- Added biome shared child style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:11:32 +02:00
CeeVeeR 5c4f7bb521 chore:
- Extracted children generator construction to createChildrenGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:10:44 +02:00
CeeVeeR 0d16f8e57f chore:
- Extracted magic value 2137 to CHILDREN_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:10:10 +02:00
CeeVeeR cf7ebcece1 chore:
- Extracted child style seed derivation to deriveChildStyleSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:09:35 +02:00
CeeVeeR 8f8a3b2355 chore:
- Extracted magic value 0.0001 to MINIMUM_CHILD_SHRINK_FACTOR

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:08:42 +02:00
CeeVeeR ffc409a4ac chore:
- Extracted magic value 2137 to CHILDREN_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:08:07 +02:00
CeeVeeR 5b5c1975c6 chore:
- Extracted generator sampling and running engine pack data to testsupport

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:07:10 +02:00
Brian Neumann-Fopiano 25c04c0157 Integrate optional terrain transforms
- Add versioned terrain providers that run before content placement.
- Resolve transformed terrain heights, occupancy, and cave metadata through bounded caches.
- Initialize native structures after binding the modded generation runtime.
- Cover transform behavior, provider resolution, and structure binding with regression tests.
2026-10-02 05:39:20 -04:00
CeeVeeR 1e25c07eac test:
- Added carving entry shared child style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:31:17 +02:00
CeeVeeR ff74a6f811 chore:
- Extracted children generator construction to createChildrenGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:30:25 +02:00
CeeVeeR 9e07009f1d chore:
- Extracted child shrink scaling to scaleByShrinkFactor

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:29:53 +02:00
CeeVeeR 53172c38ad chore:
- Extracted child style seed derivation to deriveChildStyleSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:29:21 +02:00
CeeVeeR 658c6b0fde chore:
- Extracted children generator seed derivation to deriveChildrenGeneratorSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:28:29 +02:00
CeeVeeR b998ff535a chore:
- Extracted entry id hashing to hashOfId

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:27:56 +02:00
CeeVeeR ba4708f2bb chore:
- Renamed takeSharedGenerator to createContinentalStyleGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:23:25 +02:00
CeeVeeR bda770650b fix:
- Replaced IrisGeneratorStyle.create with createNoCache in ContinentGenerator
- Removed @ExpectedToFailUntilFixed from the continent tests

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:14:52 +02:00
CeeVeeR 8bccb48c09 test:
- Added shared style generator test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:13:26 +02:00
CeeVeeR a9df19cae2 chore:
- Opened ContinentGenerator.deriveContinentSeed to the package

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:13:22 +02:00
CeeVeeR 4ac49baaff test:
- Added overworld continental style rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:56 +02:00
CeeVeeR 38a7e0fdb5 test:
- Added second rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:51 +02:00
CeeVeeR 7e73e8f599 test:
- Added rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:47 +02:00
CeeVeeR 788ac06713 test:
- Added continent zoom stretch test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:43 +02:00
CeeVeeR 622b40f786 test:
- Added ContinentGeneratorTest checking the continent map at world start

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:40 +02:00
CeeVeeR 72eff9b775 feat:
- Added ExpectedToFailUntilFixed marker and rule

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:37 +02:00
CeeVeeR 789644b93f chore:
- Replaced DimensionTerrainContext continent chain with ContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:23:14 +02:00
CeeVeeR f07dfb7a85 chore:
- Inlined IrisComplex.createContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:22:42 +02:00
CeeVeeR 5bffa6eb04 chore:
- Moved continent generator construction to ContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:22:10 +02:00
CeeVeeR 0b6ac19fa7 chore:
- Extracted continent generator construction to createContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:21:38 +02:00
CeeVeeR b7e6259a05 chore:
- Extracted shared continent generator lookup to takeSharedContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:21:05 +02:00
CeeVeeR a0aa62b8c6 chore:
- Extracted continent zoom scaling to scaleByContinentZoom

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:20:32 +02:00
CeeVeeR d80fcedd91 chore:
- Extracted continent seed derivation to createContinentSeedSource

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:19:59 +02:00
CeeVeeR 41664fc605 chore:
- Extracted magic value 234234565 to CONTINENT_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:19:27 +02:00
Brian Neumann-Fopiano 74df63f717 Merged React metric fixes
- Pregen queue reports in-flight chunk requests
- Pregen remaining reports chunks left
- Generation timings go unavailable after ten seconds without generation
2026-09-30 10:10:15 -04:00
Brian Neumann-Fopiano f46041690b Test timing freshness gate
- Test that capture drops generation timings once generation is idle
- Test that capture carries the engine timing averages while fresh
- Remove the hand-built idle snapshot test that passed without the gate
2026-09-29 18:37:15 -04:00
Brian Neumann-Fopiano b8433340c9 Split pregen queue metrics
- Publish in-flight pregen chunk requests as iris.pregen-queue
- Publish chunks left in the job as iris.pregen-remaining
- Mark generation timings unavailable after 10 seconds without generation
2026-09-29 18:09:55 -04:00
Brian Neumann-Fopiano 593c3f732a Studio hotload deadlock fix
- Live boundary capture runs without the generation history lock
- Promotion prepares the boundary and activates under the lock in separate steps
- Tab completion and find lookups stay responsive while a Studio save captures terrain
- Admission test covers history reads during the boundary checkpoint
2026-09-29 11:58:25 -04:00
Brian Neumann-Fopiano 058d527c73 VolmLib pin removed
- Local builds use the sibling VolmLib checkout for every VolmLib module with no pinned revision
- Builds without a local VolmLib use VolmLib master-SNAPSHOT like the other plugins
- The native runtime manifest and keep rules come from the embedded natives
- Iris loads only embedded natives and never downloads them
- Every jar records the VolmLib commit it was built from
2026-09-29 09:58:09 -04:00
Brian Neumann-Fopiano 15826162b4 Loader task freshness
- Always rerun the loader Exec tasks when VolmLib resolves from the published snapshot
- Read the VolmLib dirty state without taking the optional index lock
2026-09-29 09:50:38 -04:00
Brian Neumann-Fopiano 8d28ad4111 Embedded-only native loader
- Drop the repository storage and slimjar download path from NativeRuntimeLibraries
- Reject native manifests that do not use embedded storage
- Report provider hash mismatches as IOException
2026-09-29 09:33:30 -04:00
Brian Neumann-Fopiano bf11d44370 VolmLib pin removal
- The build resolves VolmLib master-SNAPSHOT when no sibling checkout is substituted
- The native runtime manifest, roots and keep rules come from the embedded natives
- The packed jar natives are verified against the embedded provider shas
- Every jar carries META-INF/volmit/volmlib.properties
- The build logs the resolved VolmLib once
- publishedNativeRuntime, the loader coordinate flags, mavenLocal and publish-volmlib are gone
2026-09-29 09:33:26 -04:00
Brian Neumann-Fopiano f60538343e Fail-closed generation fixes
- History validation always re-renders stored semantics with the current fixer
- Broken Iris worlds get a refusing generator and a banner instead of vanilla
- A failed enable locks Iris worlds instead of disabling the plugin
- Engine failures fail the chunk instead of skipping objects or features
- Modded engines close after levels unload instead of at server stopping
- Folia maintenance refuses chunks only after players leave the world
- Saved-biome loading defers chunk update passes
- Mixin audit handler names are checked against the compiled mixins
- Teleports wait up to 120 s and report the real failure
2026-09-29 08:49:50 -04:00
Brian Neumann-Fopiano 94c179fbd4 Fabric original biomes
- Capture original biomes at Fabric mod init so generated biome definitions exclude Fabric biome modifications
- Test the generated definition against a biome modified in place the way Fabric does it
2026-09-29 08:44:08 -04:00
Brian Neumann-Fopiano c7cca23155 Startup refusal fixes
- Bootstrap audits vanilla slots that hold an Iris world
- Startup stops when such a slot has unusable history
- Startup also stops when bukkit.yml no longer binds such a slot to Iris
- The audit detail carries the history read cause
- File system failures are named by type
- Bootstrap failures print the refusal banner instead of a bare stack trace
- A vanilla slot holding an Iris world is told to restore its history and keep its binding
- The vanilla slot remedy adds deleting level.dat when the level was never created
- A failed enable locks only for bukkit.yml worlds the server loads
2026-09-29 08:44:07 -04:00
Brian Neumann-Fopiano b1fbede364 Maintenance after evacuation
- Mantle-bypass maintenance starts only after players leave the world
- This covers studio close, studio disable, world remove, world unload and create rollback
- Maintenance ends only when it was entered
- Folia no longer treats a refused chunk requested during evacuation as a chunk system crash
2026-09-29 08:44:07 -04:00
Brian Neumann-Fopiano f55fde17d4 Shared teleport deadline
- Give primary world routing and Iris Vision teleports the 120 second first-chunk deadline /iris tp uses
2026-09-29 07:13:27 -04:00
Brian Neumann-Fopiano 96a400b585 Fail-closed modded decoration
- A generator with a refused or stopping engine refuses imported feature settings
- Those settings no longer fall back to vanilla ones
- Decoration fails when a sealed runtime has no settled feature table
- Decoration fails when the feature table was retired before placement
- Stacked initial spawns fail when the surface spawn biome lookup is rejected
- Those spawns no longer fall back to the chunk's top biome
- Bind failures from a stopping runtime stay out of the refusal memo and banner
- The next bind after such a failure retries
2026-09-29 07:13:23 -04:00
Brian Neumann-Fopiano 0cb3417755 Merge branch 'client-fixes' into review 2026-09-29 06:50:57 -04:00
Brian Neumann-Fopiano 8cf9e01d90 Engine failure test style
- Fixes an indentation slip and types the answer lambdas explicitly
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 2d5af3b61e Bounded teleport deadline
- /iris tp waits up to 120 seconds for the destination chunk
- A refusing generator or removed dimension still fails the command
- Tests cover the deadline and the reported timeout limit
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 782a85c26a Modded teardown fallback
- Closes engines, pools and scheduler a previous server left open when the next server starts
- Flushes them at JVM exit once the stopping server thread has ended
- Corrects the scheduler shutdown comment
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano ea516b4a59 Refused maintenance generation
- A chunk is refused while Folia world maintenance forbids mantle stages
- Such chunks no longer end up missing objects, caves, deposits or their REAL flag
- Refused chunks warn at most every 5 seconds
- The overworld mode no longer bypasses mantle stages
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano dd4633ad3a Isolated chunk update failures
- Treats only loading saved biome information as an engine failure
- Leaves a container without loot and warns once when its saved biome cannot be recovered
- Keeps a failed chunk unetched and retries it without stopping the rest of the level's update pass
- Ends a level's pass only when its engine or mantle is closed
- Covers the deferred, permanent and isolated update paths through updateChunk and the server tick
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 82fa69611e Accurate teleport failures
- Waits for the destination chunk without a wall-clock deadline so a cold first chunk no longer fails the teleport
- Reports the failure cause, an unloaded dimension, an offline player or a refused teleport separately
- Adds the new messages to every locale
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 5cadd9b8c4 Deferred saved biome updates
- Defers a chunk update pass while saved biome information is loading and retries it on a later pass
- Forgets only the updates already applied so a retry never refills a chest
- Propagates engine failures from loot application instead of marking the chunk done
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 259f885238 Modded shutdown after levels close
- Stops downloads, protocol, pregen, tools and services at server stopping
- Keeps engines, mantle, generation history, the generation pool and burst pools open while the loader drains chunk generation
- Closes each engine when its level unloads and sweeps the rest at server stopped
- Logs a failed level unload instead of aborting the loader's level close loop
2026-09-29 06:50:24 -04:00
Brian Neumann-Fopiano 54f3595ba7 Propagated engine failures
- GenerationClosedException covers closed history routers, stages, saved biome runtimes and admissions
- It also covers ownership stores, hydrology caches and engine runtimes
- GenerationFailures tells engine, lifecycle, executor and JVM failures from content failures
- Engine failures rethrow from object, cave object, procedural, floating, upper and decorator catch sites
- Biome layer, noise bound, rotation and mantle write catch sites rethrow them too
- Only content failures are skipped
- A source plan is never cached or persisted without an object a healthy engine places
- A transient engine failure no longer turns imported features off
2026-09-29 06:50:23 -04:00
Brian Neumann-Fopiano a9f07ce5c6 Merge branch 'fail-closed' into review 2026-09-29 06:34:18 -04:00
Brian Neumann-Fopiano 14f99c6df3 Merge branch 'render-drift' into review 2026-09-29 06:34:18 -04:00
Brian Neumann-Fopiano eefcbc40ef Fail-closed review fixes
- Modded logins are refused only when this save's dimension registry lists the primary world
- Otherwise Iris warns once and leaves routing idle
- An unreadable dimension registry counts as expecting the primary world
- A vanilla slot bound to Iris without Iris storage points at the bukkit.yml entry and /iris replace
- The refusal consequence comes from whether startup worlds exist instead of the first tick
- The first WorldLoadEvent or a hotload marks startup worlds as existing
- A failed enable on a hotload says the server keeps running
- Reconciler resolution failures report the refusal banner
- Operators get a join notice for such failures until the world loads
- The fail-closed generator is built even when the cause message cannot be read
- Tests drive Iris.onEnable for the lock, the unsupported-server lock and the rethrow
- The failed-enable teardown decision lives in FailedEnableLock with a test
2026-09-29 06:30:28 -04:00
Brian Neumann-Fopiano 7db46a8eb8 Forge biome modifier validation
- Pass the modded loader into the generation registry so biome modifier output stays out of validation
- Cover Forge modifiers on climate, attributes, effects and features against the canonical definition
2026-09-29 06:16:57 -04:00
Brian Neumann-Fopiano 3a4da2086e Fail-closed world generation
- Every Iris world the Bukkit resolver cannot serve gets a fail-closed generator
- CraftServer no longer receives a throw that it turns into vanilla
- A refusal banner logs the world, key, cause chain and what happens next
- The reconciler uses a throwing resolution so a broken world fails without stopping the server
- A failed enable on a server with Iris worlds keeps Iris enabled with a locked runtime
- bukkit.yml bindings in vanilla slots count as Iris worlds for that lock
- The shutdown-based vanilla fallback guard is gone
- A recorded modded bind failure rethrows instead of rebinding on every chunk call
- The first modded bind failure logs a banner
- Persistent-dimension reinject failures log a banner and repeat to operators on join
- Modded logins are refused while the configured primary Iris world is missing
2026-09-29 05:51:10 -04:00
Brian Neumann-Fopiano 3e4a2432bc Semantic registry validation
- Generated registry validation always re-renders recorded semantics with the current fixer
- Validation canonicalizes the rendering like the installer
- Renderer changes need no version bump
- The renderer identity SPI, both identity strings and the identity-based source preference are gone
- New captures record the fixer class as an informational renderer identity
- The v1 history format is unchanged
- The modded forced datapack, world check and datapack command use the runtime data version
- That is the same data version validation uses
- Tests cover a renderer change under an unchanged identity
- Tests cover 26.3 bare natural_mob_spawns history against the overlay rendering
- Tests cover semantic round trips for every supported fixer
2026-09-29 05:41:18 -04:00
Brian Neumann-Fopiano 76a07242b5 Fixed client mixin audit
- Matches the volmlib$ handler names of the client world-open and world-type mixins
- Keys expected mixins by their mixin config file and reports the target class name
- Verifies every expected handler is an injector declared by its mixin class
- Requires every registered volmlib mixin to be audited or deliberately unaudited
2026-09-29 05:36:45 -04:00
Brian Neumann-Fopiano aa428c0383 f 2026-09-29 04:43:37 -04:00
Brian Neumann-Fopiano c67af47dfe Modded 26.3
- Fabric, Forge and NeoForge target Minecraft 26.3
- Fabric uses loader 0.19.5 with Fabric API modules from 0.161.0+26.3
- Forge uses 26.3-66.0.8 with ForgeGradle 7.0.40
- NeoForge uses 26.3.0.33-beta with ModDevGradle 2.0.147
- Modded forced datapacks use the 26.3 data fixer and pack format 121
- Biome mob spawns are written as vanilla overlays
- Stronghold ring biome searches use the natural biome stream
- Chunk generation mob spawns go through the loader spawn position event
- README and the bug template list the supported versions
2026-09-29 04:11:57 -04:00
Brian Neumann-Fopiano 539cebaa21 Supported version text
- README names 26.3 as the master target
- README lists the plugin jar for 26.1.2 to 26.3
- Bug template offers 26.3, 26.2 and 26.1.2 with current loader examples
- Plugin api-version comment covers 26.1.2 through 26.3
2026-09-29 03:58:55 -04:00
Brian Neumann-Fopiano 5e95248057 Overlay biome mob spawns
- 26.3 custom biomes write natural_mob_spawns as an overlay modifier with its argument, matching vanilla biomes
- NeoForge biome modifiers can add spawns to Iris biomes again
- The 26.3 custom biome codec test checks the overlay modifier
2026-09-29 03:56:40 -04:00
Brian Neumann-Fopiano 6fd877ebfc Natural stronghold ring biomes
- The modded structure-state biome lookup resolves from the natural true biome stream, the same as the Bukkit ring search
- Stronghold ring placement no longer plans hydrology tiles around its 128 positions, so chunk structure starts stop waiting on river plans out to ~24k blocks
- Ring lookups are cached per runtime and cleared with the other biome caches
2026-09-29 03:56:40 -04:00
Brian Neumann-Fopiano 4da9661885 Loader spawn position check
- The modded chunk generator hands VolmLib the bound loader for chunk-generation creature checks
- Each loader build tests its spawn position check: Forge and NeoForge fire MobSpawnEvent.PositionCheck and honor a denial, Fabric applies the vanilla rule and obstruction checks
2026-09-29 03:56:40 -04:00
Brian Neumann-Fopiano 92e5daa13e Modded 26.3 datapacks
- The forced datapack, datapack commands, and world check render biomes and dimension types with the 26.3 data fixer and pack format 121
- Modded tests use the 26.3 feature, placement, spawner, sign, and biome resolver APIs
- The water spawn test binds a blocks_motion fixture so solid waterlogged blocks still obstruct spawns
2026-09-29 02:48:58 -04:00
Brian Neumann-Fopiano 94c7181aec Modded 26.3 targets
- Fabric, Forge, and NeoForge build against Minecraft 26.3 with Fabric Loader 0.19.5, Forge 26.3-66.0.8, and NeoForge 26.3.0.33-beta
- Fabric API modules follow fabric-api 0.161.0+26.3, ForgeGradle is 7.0.40, and ModDevGradle is 2.0.147
- Loader metadata requires Fabric Loader 0.19.5, Forge 66, NeoForge 26.3, and Minecraft 26.3
- The mod resource pack declares formats 97.1 through 121.0
- The v26_3_R1 headless sources no longer unpack a version overlay
- The README lists the 26.3 loader targets
2026-09-29 02:48:50 -04:00
Brian Neumann-Fopiano b7626ab23f Column biome fallback
- Column biome hunk takes its fallback biome from the caller
- The fallback resolves on first unset read
- Bukkit chunk generator supplies plains
- Core keeps no new Bukkit coupling
2026-09-29 01:12:16 -04:00
Brian Neumann-Fopiano 08ffb96680 Modded registry factories
- Modded platform gets registry access from the modded server factory
- Engine bootstrap and tile parity test get tile readers from the modded server factory
- Registry access tests build through the modded server factory
2026-09-29 01:12:16 -04:00
Brian Neumann-Fopiano d6226240a0 Dropped minecraft-common scope
- Generation revision no longer requires the removed minecraft-common sources
- Modded shared inputs drop the removed module
- Loader source sets no longer add the removed module
- README points shared Minecraft-native code at VolmLib
2026-09-29 00:50:30 -04:00
Brian Neumann-Fopiano 85849693b6 Pregen throughput work
- Object source plans are packed per destination and built once
- Source plans retire once every destination in reach consumed them
- Rotated block states and property variants are memoized
- Generation history syncs to disk from a background group commit
- History syncs at world save, shutdown, pregen end and promotion
- History capture reuses per-column biome and mantle reads
- Semantic summaries are built only when read
- Hydrology plans the whole pregen area nearest first
- Chunks without hydrology skip composition and cave cell lookups
- Server-thread stand-in hydrology answers are never cached
- Neighbour drafts stay inside the planned pregen area
- Terrain3D caches are lock-free
- Cave density lattice samples are shared between neighbouring chunks
- The eager carve window is 27 by 27 chunks with on-demand terrain beyond it
- Cave cells are copied once per chunk for the carve stage
- Terrain columns are painted z-major with per-level ore bands
- Deposit clumps that cannot place are rejected before building
- Dead biome inject writes are gone
- Chunk output buffers track each column top
- Engine getters use a thread-id frame table
- Pregen keeps region border chunks until neighbouring regions finish
- Mantle reads take plate pins so saves cannot close plates in use
2026-09-29 00:37:46 -04:00
Brian Neumann-Fopiano 319d249445 History sync test drain stub
- Close test stubs the admission drain with the deadline signature
- Stubbed drain reports every request drained
2026-09-29 00:33:13 -04:00
Brian Neumann-Fopiano 73af3efe12 Merge master
# Conflicts:
#	core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java
2026-09-29 00:30:12 -04:00
Brian Neumann-Fopiano 6c7796f12f Pregen failure handling
- Chunk generator reports generation failures to the running pregen job
- Pregen methods forward generation failures to the async method
- Async pregen fails every outstanding request through an in-flight ledger
- In-flight requests settle once and ignore late completions
- Pregen aborts with the failing chunk as its cause
- Admission drain waits with a 60 second deadline
- Closing the admission gate fails leftover requests
- Tests cover failure propagation, the ledger and the drain deadline
2026-09-29 00:29:34 -04:00
Brian Neumann-Fopiano 3722602a78 Transition dry layer fix
- Dry layers keep head and ceiling equal to the bed after the transition taper
- Displaced layers that dry out also keep head and ceiling at the bed
- Transition displacement reads its datum from the original fluid head
- Tests cover dry banks and drying channels across a transition band
2026-09-29 00:29:34 -04:00
Brian Neumann-Fopiano 7b06cd51ac Modded test fixes
- Language asset test locates the directory through the English file
- Structure template test reads the mixin class bytes instead of loading it
- Water spawn test initializes block state caches before running
- Logging policy check runs after the native sources are unpacked
- Native class boundary check runs after classes compile
2026-09-29 00:29:26 -04:00
Brian Neumann-Fopiano 34684da611 Modded pack metadata
- Modded jars ship pack.mcmeta for client resources and mod data
- Modded jars bundle jlibdeflate and its native libraries
- Artifact check requires pack.mcmeta and the jlibdeflate entries
- Artifact check rejects client language resources without pack.mcmeta
- Test loads the pack metadata against the current Minecraft formats
2026-09-29 00:29:26 -04:00
Brian Neumann-Fopiano c20296d106 Modded server registries
- Modded platform hands the running server to registry access
- Tile reader is built from the running server
- Registry test covers startup, reload and shutdown of dynamic registries
2026-09-29 00:29:25 -04:00
Brian Neumann-Fopiano ce1aad8a35 Revert per-chunk hydrology samples
- painter read of the chunk column array changed bank and shore cut materials (three terrain actuator tests)
2026-09-28 18:52:06 -04:00
Brian Neumann-Fopiano e4f20a7f7f Merge W3-polish 2026-09-28 18:49:32 -04:00
Brian Neumann-Fopiano bcb59d3584 Merge W2H-platerace 2026-09-28 16:20:12 -04:00
Brian Neumann-Fopiano 7ac5ffdb81 Per-chunk hydrology samples
- terrain painter reads the chunk's column array once instead of 256 cache lookups
- test pins chunk samples against per-column lookups on a real plan
2026-09-28 15:54:23 -04:00
Brian Neumann-Fopiano 6202ae5d06 Cheaper ellipsoid deposit cells
- squared y/z offsets computed once per sphere row, same evaluation order
- test pins cells, order and RNG draws against the per-cell form
2026-09-28 15:54:22 -04:00
Brian Neumann-Fopiano 59165df427 Take chunk uses through pins
- Every getChunk(..).use() site calls Mantle.useChunk, which cannot fail because a save closed the chunk in between
- Covered-chunk cleanup, custom block and static object layers take their chunk through useChunk
- Discovered flag writes go through Mantle.flag instead of an unpinned chunk
- Test mantles stub useChunk the way the real one behaves
2026-09-28 15:46:54 -04:00
Brian Neumann-Fopiano 6bcf90bb79 Merge W2G-paperio 2026-09-28 15:27:14 -04:00
Brian Neumann-Fopiano ca85cd31b7 Retain pregen dependency chunks
- PregenChunkRetention holds every chunk in a FULL request's dependency square until all in-area regions reaching it drained; drained regions keep only the border strips undrained neighbours need
- Removes the unload/reload churn (about 1.5 reloads per generated chunk at r1000: lattice passes, next region's border, future-region and ring protos)
- Capped at one chunk per MB of heap; over the cap the drained strips farthest from the active region are released; heap pressure and close release everything
- Regions still save and unload at drain; the early-unload test now pins that plus border retention
2026-09-28 15:20:14 -04:00
Brian Neumann-Fopiano 8899c9e3f9 Merge W2F-hydroplanner 2026-09-28 15:02:08 -04:00
Brian Neumann-Fopiano 491382abaa Merge W2C-terrain 2026-09-28 14:44:12 -04:00
Brian Neumann-Fopiano b4ee500c14 Restore evicted owners from plans
- A demanded owner missing from the owner cache is restored from its planned tile (resident or prepared-plan store) instead of drafted again
- Same restore path as persisted tiles; plans identical
2026-09-28 14:34:37 -04:00
Brian Neumann-Fopiano 0695a1d707 Merge W2D-mantleio 2026-09-28 14:25:12 -04:00
Brian Neumann-Fopiano 0b2a606a25 Hydrology plan digest probe
- Plans real pack tiles headless and prints a digest per tile with planning CPU
- Optional pregen radius plans them under a pregeneration scope and prepared-plan store
- Expected-digest file fails the run on any changed plan
2026-09-28 14:21:59 -04:00
Brian Neumann-Fopiano f3b16b784b Merge W2B-carving 2026-09-28 14:15:03 -04:00
Brian Neumann-Fopiano cb31510b89 Skip quiet block update checks
- ChunkBlockUpdateListener remembers the last state with no update or custom effect
- Plain terrain runs skip the per-block isUpdatable and isCustom checks
- Test replays random writes against the per-write catch semantics
2026-09-28 14:13:20 -04:00
Brian Neumann-Fopiano 7a03df5c82 Merge W2A-history 2026-09-28 14:10:07 -04:00
Brian Neumann-Fopiano adbcba3efc Batch history biome reads
- saved biome capture resolves each quart column through one engine call
- column-invariant height, region, surface and cave fallback resolved once
- cavern reads use one pinned mantle chunk per column
- saved biome cells interned per capture
- engine getters borrow the bound route runtime for other chunks instead of opening a stage
2026-09-28 14:04:36 -04:00
Brian Neumann-Fopiano 0ee0f62b9d Limit early owner drafts in pregen
- Pregeneration scope limits the lower-rank neighbour drafts an owner starts early to tiles the area composes from
- Undemanded ring drafts (and the early neighbours they started) are no longer computed; demanded ones still resolve on demand
- r2000 tile set: 80 -> 54 drafts, planning CPU -32%, plans identical
2026-09-28 13:58:58 -04:00
Brian Neumann-Fopiano 55e4a70b3b Drop stale cave snapshots
- dimension stack metadata clearing and continuation restore rewrite cavern cells after sCave, so they drop the context snapshot and decoration re-reads the mantle
- test that decoration reuses only its own chunk's snapshot
2026-09-28 13:55:18 -04:00
Brian Neumann-Fopiano 53b36fe197 Resident plate lookups
- Loaded-plate probes use Mantle.getLoadedRegion instead of the region map
2026-09-28 13:51:05 -04:00
Brian Neumann-Fopiano 195f1862af Wall scope test via real matter
- feeds caverns through a real mantle section now that sCave snapshots the chunk instead of iterating it
2026-09-28 13:50:36 -04:00
Brian Neumann-Fopiano c00dfbddce Cheaper cave biome resolution
- carving root entries cached per world Y in arrays instead of a boxed-key map
- semantic cave capture skips re-adding the key it added last
2026-09-28 13:43:39 -04:00
Brian Neumann-Fopiano 2fef055e5c Stream terrain boundary capture
- boundary columns build runs directly instead of voxel lists, tree sets and hash maps
- palette sorted once per column with a direct comparator, same canonical order
- geometry source reuses the last encoded voxel
- equivalence test against the previous encoding
2026-09-28 13:43:39 -04:00
Brian Neumann-Fopiano 20e10999dd Lazy semantic region summaries
- claims cache the next region shard without rebuilding its key summary
- summaries are computed when a query or compaction reads them
2026-09-28 13:43:38 -04:00
Brian Neumann-Fopiano 2ac9609407 Cheaper block writes and palette lookups
- ColumnExtentListeningHunk reports writes through a primitive listener instead of boxing ListeningHunk
- Palette layer generators memoized per seed slot instead of one shared recent entry
2026-09-28 13:41:08 -04:00
Brian Neumann-Fopiano f4a06efa21 Terrain column painter
- Paint chunks z-major so neighbouring columns share block-buffer cache lines
- Fill the solid run below layers, river paint and cave ceilings with ores or rock in one loop
- Resolve chunk constants, air and bedrock once per chunk; drop the unread shared lastBedrock write
- IrisOreBands replaces IrisOreGeneratorBounds: per-Y candidate generators in list order
- Resolve a constant (FLAT) ore chance once instead of sampling it per block
- Column parity test against the per-block algorithm over randomized columns
2026-09-28 13:41:08 -04:00
Brian Neumann-Fopiano df299bb97d Cheaper mantle writer close
- close reads each window slot before the atomic swap instead of swapping all 5,929 slots
- terrain-ready flags live with the terrain access and exist only on content writers
2026-09-28 13:36:39 -04:00
Brian Neumann-Fopiano 2930ca40fb LRU plate eviction in pregen
- Over the plate cap, the least recently used idle plates are saved one at a time on the IO pool while submissions continue
- Submissions wait only past the cap plus a small overshoot
- Replaces trimming and unloading every idle plate, which evicted the active working set and read it back
2026-09-28 13:34:33 -04:00
Brian Neumann-Fopiano 0052dfec39 Trimmed mantle section writes
- The section adapter writes without re-trimming; the chunk writer trims right before
2026-09-28 13:34:33 -04:00
Brian Neumann-Fopiano e31113aaa2 Captured cave biomes in content
- decorateNaturalCaves resolves coordinates of its own chunk through the captured column path; other coordinates keep the history dispatch
2026-09-28 13:32:53 -04:00
Brian Neumann-Fopiano 808e133cac Carve terrain on demand
- eager terrain window covers the replayed object sources plus one chunk of footprint (radius 13 instead of 19 for overworld)
- content writers generate a chunk's missing terrain components before handing it out, so reads past the window stay identical
- getEagerInputRadius replaces isInputGenerationLazy
2026-09-28 13:32:53 -04:00
Brian Neumann-Fopiano 8d93431319 Skip empty cave slice copies
- snapshot copies only present palette values and drops sections without cells
2026-09-28 13:32:53 -04:00
Brian Neumann-Fopiano 184a04d97f Snapshot cave terrain per chunk
- sCave copies the chunk's cavern and hydrology terrain view once (journal originals win) instead of per-voxel monitor, journal and slice lookups
- hydrology lookups answer null without work in chunks with no cave cells
- decorateNaturalCaves reuses the sCave snapshot through the chunk context
2026-09-28 13:11:28 -04:00
Brian Neumann-Fopiano 08353e3bdd Merge L1-objects 2026-09-28 12:26:54 -04:00
Brian Neumann-Fopiano 9d3597703d Merge L3-hydrology 2026-09-28 12:20:03 -04:00
Brian Neumann-Fopiano c4dcb32334 Merge L5-output 2026-09-28 12:19:03 -04:00
Brian Neumann-Fopiano cd61b75ff6 Merge L4-terrain 2026-09-28 12:19:03 -04:00
Brian Neumann-Fopiano 83c6c66294 Merge L2-pipeline 2026-09-28 12:19:03 -04:00
Brian Neumann-Fopiano 1614d6a9a7 Remember neighbourhood answers
- Each thread keeps its recent final hasColumnsAround answers next to its chunk slots, so the object and hydrology gates skip the nine-chunk scan on repeat
2026-09-28 12:05:05 -04:00
Brian Neumann-Fopiano af2167e2f7 Hide rotation memo accessors
- Keep the rotated-state memo out of the Lombok getter and toString
2026-09-28 11:57:34 -04:00
Brian Neumann-Fopiano 2d5da0076c Unboxed base terrain height
- Base height stream samples through the primitive double stream
2026-09-28 11:57:07 -04:00
Brian Neumann-Fopiano dcdda0d3c7 Hoist cave scan column top
- read the column top once per column and again only after zone decoration writes
2026-09-28 11:57:02 -04:00
Brian Neumann-Fopiano 34f00d86cf Thread-id table for engine frames
- frames live in a 256-slot table indexed by thread id, ThreadLocal only on slot collisions
- the single ThreadLocal probed long collision chains in the workers' thread-local maps
2026-09-28 11:57:02 -04:00
Brian Neumann-Fopiano 82f52cd6ec Skip empty chunk compositions
- Queued and demand compositions of chunks the occupancy index already knows are empty return without loading tiles
2026-09-28 11:55:13 -04:00
Brian Neumann-Fopiano 14f3a263df Retire consumed source plans
- Count the destinations in reach that asked for each source
- Drop the entry once all of them did, so only the frontier stays cached
2026-09-28 11:50:13 -04:00
Brian Neumann-Fopiano d59517aeaa Hydrology occupancy and lookahead
- Index planned tiles by occupied chunk; empty chunks skip composition, tile residency and cave-cell lookups
- Plan the whole pregen area nearest first with several roots, past the tile budget when plans persist
- Restore persisted tiles without a planning root permit
- Never memoize answers the server thread gave before its hydrology plan landed
2026-09-28 11:45:53 -04:00
Brian Neumann-Fopiano a35ecca7bd Group commit generation history
- Semantic journal and saved-biome appends write without forcing; a shared flusher forces touched files within 2s outside every store lock
- GenerationHistory.sync forces pending history at world save, engine shutdown, pregen completion and before activation promotion
- A failed deferred force poisons the store the same way an uncertain append did
- Cache the active epoch pack root instead of regex-validating it on every stage open
- Tests pin append-without-force, sync barriers, failure poisoning and the save, shutdown and pregen hooks
2026-09-28 11:41:17 -04:00
Brian Neumann-Fopiano 066017cdf6 Read ore chance cache first
- Skip the capturing supplier allocation once the chance noise exists
2026-09-28 11:38:41 -04:00
Brian Neumann-Fopiano 8878e9aa5f Spread native volume cache keys
- Mix chunk keys so origin and query caches stop treeifying buckets
2026-09-28 11:36:50 -04:00
Brian Neumann-Fopiano ec1984949b Reject deposit clumps early
- Ellipsoid footprint dry run replays the build draws without building
- Terrain-scoped clumps above every column limit are dropped before building
- Surface biome filters no chunk column passes skip the deposit
2026-09-28 11:35:56 -04:00
Brian Neumann-Fopiano f2236a8482 Skip floating fields for plain parents
- parent biome lookups read the biome stream directly instead of building the boundary field
- fields are only built where a parent with floating children needs edge distances
2026-09-28 11:26:41 -04:00
Brian Neumann-Fopiano 1df51b230d Column biome output buffer
- Bukkit noise generation keeps one biome per column until a partial column write
- drops the full-resolution biome array allocation and fill per chunk
2026-09-28 11:23:24 -04:00
Brian Neumann-Fopiano 2f8d3b1d1a Memoize object block states
- Cache vanilla withProperty variants per state
- Cache rotated states per rotation and quantized spin
- Hoist the stilt dirt state and rotate only target-layer stilt blocks
2026-09-28 11:22:51 -04:00
Brian Neumann-Fopiano d0008086ac One thread frame per engine
- runtime assembly, biome environment and runtime scope share one ThreadLocal frame
- engine getters do a single ThreadLocal lookup
2026-09-28 11:17:51 -04:00
Brian Neumann-Fopiano badca54e5d Plain block state flags
- lazily computed BukkitBlockState flags are bytes instead of volatile Booleans
2026-09-28 11:17:51 -04:00
Brian Neumann-Fopiano ec07f55fd8 Pack and drain object plans
- Store source plans as packed per-destination cell runs over a value table
- Weigh shared payloads once and give tiles and markers finite weights
- Release a plan once every touched destination consumed it, keeping its index
- Rebuild only on repeated consumption; debug stats for builds and waits
2026-09-28 11:12:37 -04:00
Brian Neumann-Fopiano b47c7ae6aa Skip impossible deposit variants
- Scan pack once per data load for declared depositVariants
- Placed ores skip the per-block cave biome lookup when none exist
2026-09-28 11:11:20 -04:00
Brian Neumann-Fopiano bc618e20ec Plain chunk output buffer
- ChunkDataHunkHolder stores cells in a plain array and tracks each column's highest stored y
- natural cave decoration and perfection stop scanning above the column top
2026-09-28 11:11:11 -04:00
Brian Neumann-Fopiano 2ce36eb6f1 Share cave density lattice
- Adaptive plane lattice samples cached per carver across neighbour chunks
- Grid cleared with carvers on hotload
2026-09-28 11:06:34 -04:00
Brian Neumann-Fopiano 631c486a5a Lock-free Terrain3D caches
- Replace synchronized move-to-last LRU stripes with chunk-block CLOCK cache
- Copy-on-write compiled profile map, lock only on miss
- Raise column cache to 2048 chunks
2026-09-28 11:06:34 -04:00
Brian Neumann-Fopiano 8b5c109d55 Sample noise biome cache size
- check the cache bound on a few quarts per column instead of every insert
2026-09-28 11:05:42 -04:00
Brian Neumann-Fopiano 3df1fba518 Drop dead biome inject writes
- sBiome, floating biomes and publishNaturalMetadata no longer write MatterBiomeInject; nothing reads it
- stack metadata clearing only runs for dimension stacks
2026-09-28 11:05:42 -04:00
Brian Neumann-Fopiano 241260a762 Support latched tree felling
- Add a run hook controlling continued sneaking.
- Preserve latched runs when players release sneak.
2026-09-28 07:03:58 -04:00
Brian Neumann-Fopiano c706eab60f Merged pregeneration reliability fixes
- Integrate hydrology persistence, retry, and memory-management changes.
- Preserve terrain-transition object continuations with byte-based cache accounting.
- Verify merged object plans, continuation replay, and cache behavior.
2026-09-28 02:15:30 -04:00
Brian Neumann-Fopiano 21f8440299 Stabilized hydrology pregeneration
- Preserve exact river owner plans across cache eviction and persistence.
- Retry required hydrology plans without generating riverless terrain.
- Bound object plans and pregeneration residency by available heap.
- Release completed native chunks and avoid empty reclamation flushes.
- Cover persistence, admission, cancellation, and ownership regressions.
2026-09-28 02:13:17 -04:00
Brian Neumann-Fopiano 8c3e5d547b Reworked terrain transitions
- New terrain heights meet saved boundaries while retaining local relief
- Surface water levels approach saved fluid heights across the band
- Cave and physical biome matching use a four-block seam patch
- Terrain generates once without speculative neighbor containment
- Bounded caches serve transition membership and boundary lookups
- Saved biome writes honor the selected durability policy
- Release labels and presentation code leave generation identity unchanged
- Unfinished objects retain blocks and metadata across pack changes
- Whole structure assemblies preserve pending excavation and foundations
- New placements keep pending historical pieces clear
- Transition probes cover cutovers and retained object fragments
2026-09-27 15:31:31 -04:00
Brian Neumann-Fopiano 9eb05fd987 Memory patch 2026-09-26 09:15:03 -04:00
Brian Neumann-Fopiano 201472b635 Merged packaging updates
- Reviewed packaging changes are integrated into the main checkout
2026-09-26 08:59:53 -04:00
Brian Neumann-Fopiano 45bc9cb3f7 Merged plugin packaging
- Automatic jar selection and version updates are integrated
2026-09-26 08:58:59 -04:00
Brian Neumann-Fopiano 0837842f5c Updated Iris packaging
- Version is 4.1.2-26.2 across platform builds
- Plugin builds use shared automatic jar selection
- Native adapters remain bundled in selected artifacts
- Plugin export tasks copy the selected artifact
- The previous plugin packer is removed
- The agent tracks primary storage closure during Spigot shutdown
2026-09-26 03:55:40 -04:00
Brian Neumann-Fopiano 68aa6adec7 Testing Jar Packing for minimization 2026-09-24 06:46:27 -04:00
Brian Neumann-Fopiano 38cf4ac7c7 Optimize world generation
- Limit terrain cleanup to affected columns and batch cave boundary reads.
- Reuse offline generation workers and native air states.
- Gather history writes and defer unpublished append synchronization until atomic publication.
- Cover terrain parity, worker lifecycle, history durability, and publication failures.
2026-09-23 21:32:17 -04:00
Brian Neumann-Fopiano 7986698c1c fix builds 2026-09-23 08:02:16 -04:00
Brian Neumann-Fopiano ca9f28c9b0 Expanded terrain generation tooling
- Standalone Minecraft 26.3 generation uses native registries, biome planning and structure planning with bounded concurrency
- Offline export publishes fresh TERRAIN checkpoints for native server completion
- Checkpoints retain mantle data, immutable generation history and captured server configuration
- Output publication uses staged directories, durability checks and atomic moves
- Offline distribution tasks provide an installed launcher and reusable native fixtures
- Dimension settings control terrain, biome-bound and cave-density sampling with unchanged defaults
- Pack validation and schemas enforce the supported sampling intervals
- Biome layer caches isolate data, engine and seed identities
- Floating biome palettes keep stable thickness patterns across generation orders
- Hydrology batches terrain samples within the owning generation runtime
- Hydrology planners share bounded basis caches and reuse raster geometry and distance samples
- Demand planning admits new tiles as active plans finish
- Volumetric height queries avoid unnecessary density evaluation and bound cached anchors
- Cave generation reuses chunk-scoped inputs and semantic capture
- Object replay indexes mutations by destination and reuses resolved records
- Source-plan caches coordinate concurrent builds and reduce overlay allocation
- Prerequisite carving reads preserve the underlying hydrology before object placement
- Tree material decoding preserves stored native block identities
- Engine shutdown drains generation outside the lifecycle lock and coordinates runtime retirement
- Pregeneration sizes temporary worker boosts from processor and heap capacity
- Pregeneration restores native worker counts after drain and failure paths
- Native structure queries retain per-origin coordination without broad window locks
- Saved biome writes drain pending batches under their write stripe
- Boundary column lookups avoid repeated voxel decoding
- Modded registry consumers use explicit VolmLib providers and version-specific native source overlays
- Regression tests cover generation parity, cache isolation, concurrency, shutdown, persistence and offline checkpoints
2026-09-23 07:00:55 -04:00
Brian Neumann-Fopiano 98ddb6644d Improved saved world startup
- Startup holds generation admission through recovery and runtime attachment
- Prepared startup state removes the second recovery scan before generator readiness
- Recovery batches allocated chunks by region with reusable snapshot buffers
- Region decoding uses up to four workers within processor and heap limits
- Oversized regions and external chunks use bounded direct reads
- Status parsing validates chunk coordinates without constructing the full NBT tree
- Region snapshots reject changed files, allocation headers and truncated payloads
- Native zlib decompression uses jlibdeflate with a Java inflater fallback
- Chunk readers reuse region handles and decompression buffers during recovery and boundary capture
- Semantic shards reuse validated summaries with invalidation on mutation
- Semantic key validation avoids regular expression allocation
- Saved biome indexes use buffered sequential reads with reusable CRC state
- Pack inspection reads validated manifest and pack state without opening terrain indexes
- Datapack compilation reuses pack inspection within each operation
- Kernel staging avoids duplicate pack hashing when no activation is pending
- Pack copying and directory synchronization use up to four available processors
- Boundary publication batches directory synchronization around durable catalog publication
- Configured world reconciliation reuses ready runtimes after identity and saved-state checks
- Explicit world loads retain the full validation path
- Regression tests cover startup admission, recovery, malformed data, resource limits, publication and reconciliation
2026-09-23 06:56:45 -04:00
Brian Neumann-Fopiano 30c533feaf Tunabelly 2026-09-22 17:04:56 -04:00
Brian Neumann-Fopiano 197d2c2461 Tuning 2026-09-21 10:04:22 -04:00
Brian Neumann-Fopiano 8e3ef1b651 Poplar 2026-09-20 16:18:01 -04:00
Brian Neumann-Fopiano 2fa0853e42 [VERY VERY VERY BAD IDEA] 2026-09-20 04:24:42 -04:00
Brian Neumann-Fopiano f0e7fea13b Test- 26.3 2026-09-19 23:48:44 -04:00
Brian Neumann-Fopiano edadd14f4b Iris downloader / History update
- Allow people to download in place and update in place the media
2026-09-18 21:52:19 -04:00
1908 changed files with 128876 additions and 56300 deletions

No files matched your search

+56 -63
View File
@@ -1,85 +1,78 @@
name: Iris Bug Report
description: File a bug report for Iris
labels: [ bug ]
name: Bug report
description: Something in Iris isn't working.
labels: [bug]
type: Bug
assignees: [SwiftSwamp]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill this out!
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
Make sure you're on the [latest release](https://github.com/VolmitSoftware/Iris/releases/latest) and that it hasn't [already been reported](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue).
The Iris plugin supports Minecraft 26.1.2 through 26.3 on Paper, Purpur, Leaf, Canvas, Folia, and Spigot. The Fabric, Forge, and NeoForge builds support 26.3. Both need Java 25.
- type: textarea
id: how
id: issue
attributes:
label: Problem
description: Please give a text description of how you reached the problem
value: |
1. Install Iris...
2. Do this...
3. Do that...
4. Observe the error...
label: What issue are you having?
description: Include any error message you saw.
validations:
required: true
- type: textarea
id: what
id: steps
attributes:
label: Solution
description: Explain where you think the problem comes from (optional)
placeholder: The code to place a is missing b and c...
label: Steps to reproduce
placeholder: |
1. Start a server with Iris installed
2. Change ... in the config
3. Run /iris ...
4. ... happens instead of ...
validations:
required: false
required: true
- type: input
id: diagnostics
attributes:
label: Debug dump or latest log file
description: Run `/iris debug dump upload=true` and paste the link. If Iris won't start, upload `logs/latest.log` to [mclo.gs](https://mclo.gs) and paste that link instead.
placeholder: https://mclo.gs/...
validations:
required: true
- type: input
id: plugin-version
attributes:
label: "Iris version"
description: Run `/iris debug version` (`/iris version` on Fabric, Forge, and NeoForge).
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
label: Server software
options:
- Paper
- Purpur
- Leaf
- Canvas
- Folia
- Spigot / CraftBukkit
- Fabric
- Forge
- NeoForge
- Other (describe under Problem)
validations:
required: true
- type: dropdown
id: mcversion
attributes:
label: Minecraft Version
description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported.
options:
- '26.2'
- "Paper"
- "Purpur"
- "Leaf"
- "Canvas"
- "Folia"
- "Spigot"
- "Fabric"
- "Forge"
- "NeoForge"
- Other (unsupported)
validations:
required: true
- type: input
id: loaderversion
- type: dropdown
id: minecraft
attributes:
label: Platform / Loader Version
description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta".
placeholder: DO NOT SAY "LATEST"
label: Minecraft version
options:
- "26.3"
- "26.2"
- "26.1.2"
- Other (unsupported)
validations:
required: true
- type: input
id: irisversion
- type: textarea
id: extra
attributes:
label: Iris Version
description: What version of Iris are you running? (see console)
placeholder: DO NOT SAY "LATEST"
validations:
required: true
- type: input
id: logs
attributes:
label: Log
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
placeholder: https://mslog.gs/...
validations:
required: true
- type: markdown
id: thanks
attributes:
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
label: Anything else?
description: Other plugins involved, screenshots, or videos.
+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.
+5
View File
@@ -76,3 +76,8 @@ BUILDS/
.compatwt/
.worktrees/
/VolmLib/
/docs/
/.perf/
/.codegraph/
+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>.
+26 -13
View File
@@ -4,7 +4,7 @@ Iris is a world generation engine for Minecraft servers and mod loaders. It gene
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
or NeoForge server mod. Cross-platform generation targets deterministic parity for identical
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2.
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.3.
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions)
@@ -20,12 +20,12 @@ Canonical English is defined in the typed Java catalogs under `core/src/main/jav
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set |
| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 |
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.3 | Full feature set |
| Folia | plugin jar | 26.1.2 - 26.3 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.3 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; requires NeoForge 26.3.0.65-beta+; current target is NeoForge 26.3.0.83 |
Java 25 is required on every platform.
@@ -252,7 +252,9 @@ Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everyth
./gradlew buildAllToOut
```
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
builds every platform artifact into the workspace-level `../PluginOuts/` directory: the Bukkit jar
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
(1.21.1, 26.1.2, 26.2 and 26.3):
```
Iris v<version> [CraftBukkit] <mc>.jar
@@ -261,16 +263,26 @@ Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
Per-platform tasks: `./gradlew buildBukkit`, and `build<Loader><mc>` for one mod jar, for example
`buildFabric26_2` or `buildNeoForge1_21_1`. `buildFabric`, `buildForge` and `buildNeoforge` export the
26.3 jars. `./gradlew checkAll` runs every check, then builds and verifies all twelve mod jars and runs their
unit tests; Forge 1.21.1 only compiles its tests because Forge 52 cannot bootstrap Minecraft outside
FML. The
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
changes only the consumer directory. Both tasks copy all four verified platform jars into
changes only the consumer directory. Both tasks copy every verified platform jar into
`../PluginOuts/`.
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
`-PuseLocalVolmLib=false`, the build uses VolmLib `master-SNAPSHOT` from JitPack. There is no VolmLib
version to bump. The build logs which VolmLib it used, and every jar records it in
`META-INF/volmit/volmlib.properties`.
If you need help compiling as a developer or contributor, ask in the Discord.
## Adapters / modded development
@@ -298,9 +310,10 @@ properties:
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
screens); every adapter adds those source directories, so one edit reaches all three loaders.
Shared code lives in `adapters/modded-common` (loaders + the shared test suite) and
`adapters/client-common` (client HUD and world-type screens); every adapter adds those source
directories, so one edit reaches all three loaders. Shared Minecraft-native code lives in VolmLib
(`native/minecraft-common`).
For IDE import you can surface the three adapter builds in the root composite with
`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to
@@ -1,509 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class DatapackStructureStateFilter {
private DatapackStructureStateFilter() {
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl());
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures
) {
return filter(
structureSets,
scopeIndex,
declaredSources,
importedStructures,
keyIndex(structureSets));
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (scopeIndex.isEmpty()) {
return new Selection(
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
0,
0);
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity = new IdentityHashMap<>();
Set<Holder<StructureSet>> visiting = Collections.newSetFromMap(new IdentityHashMap<>());
List<Holder<StructureSet>> filteredSets = new ArrayList<>(structureSets.size());
int retainedManagedSets = 0;
int excludedManagedSets = 0;
for (Holder<StructureSet> holder : structureSets) {
String setKey = structureSetKey(holder, keyIndex);
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
Holder<StructureSet> scopedHolder = scopeHolder(
holder,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (scopedHolder == null) {
if (managedSet) {
excludedManagedSets++;
}
continue;
}
if (managedSet) {
retainedManagedSets++;
}
filteredSets.add(scopedHolder);
}
return new Selection(
scaleFrequencyOverrides(
List.copyOf(filteredSets), importedStructures, keyIndex),
retainedManagedSets,
excludedManagedSets);
}
static String structureSetKey(Holder<StructureSet> holder) {
return structureSetKey(holder, keyIndex(List.of(holder)));
}
static StructureSetKeyIndex keyIndex(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
return StructureSetKeyIndex.create(registeredStructureSets);
}
private static String structureSetKey(
Holder<StructureSet> holder,
StructureSetKeyIndex keyIndex
) {
return keyIndex.resolve(holder)
.map(key -> key.identifier().toString())
.orElse(null);
}
private static List<StructureSet.StructureSelectionEntry> filterEntries(
List<StructureSet.StructureSelectionEntry> entries,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
List<StructureSet.StructureSelectionEntry> filtered = new ArrayList<>(entries.size());
for (StructureSet.StructureSelectionEntry entry : entries) {
String structureKey = entry.structure().unwrapKey()
.map(key -> key.identifier().toString())
.orElse(null);
if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) {
filtered.add(entry);
}
}
return filtered;
}
private static Holder<StructureSet> scopeHolder(
Holder<StructureSet> holder,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
if (scopedByIdentity.containsKey(holder)) {
return scopedByIdentity.get(holder);
}
if (!visiting.add(holder)) {
return null;
}
try {
String setKey = structureSetKey(holder, keyIndex);
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
scopedByIdentity.put(holder, null);
return null;
}
StructureSet originalSet = holder.value();
List<StructureSet.StructureSelectionEntry> entries = filterEntries(
originalSet.structures(), scopeIndex, declaredSources);
if (entries.isEmpty()) {
scopedByIdentity.put(holder, null);
return null;
}
StructurePlacement placement = scopePlacement(
originalSet.placement(),
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (entries.size() == originalSet.structures().size()
&& placement == originalSet.placement()) {
scopedByIdentity.put(holder, holder);
return holder;
}
Holder<StructureSet> scoped = replacementHolder(
holder, new StructureSet(entries, placement), keyIndex);
scopedByIdentity.put(holder, scoped);
return scoped;
} finally {
visiting.remove(holder);
}
}
static Optional<StructurePlacement.ExclusionZone> exclusionZone(StructurePlacement placement) {
Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class);
if (value instanceof Optional<?> optional && (optional.isEmpty()
|| optional.get() instanceof StructurePlacement.ExclusionZone)) {
@SuppressWarnings("unchecked")
Optional<StructurePlacement.ExclusionZone> resolved =
(Optional<StructurePlacement.ExclusionZone>) optional;
return resolved;
}
throw new IllegalStateException("Could not read structure placement exclusion zone from "
+ placement.getClass().getName());
}
private static StructurePlacement scopePlacement(
StructurePlacement placement,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
Optional<StructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
if (currentZone.isEmpty()) {
return placement;
}
Holder<StructureSet> target = currentZone.get().otherSet();
String targetKey = structureSetKey(target, keyIndex);
if (targetKey != null) {
target = holdersByKey.getOrDefault(targetKey, target);
}
Holder<StructureSet> scopedTarget = scopeHolder(
target,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
Optional<StructurePlacement.ExclusionZone> scopedZone = Optional.empty();
if (scopedTarget != null) {
scopedZone = Optional.of(new StructurePlacement.ExclusionZone(
scopedTarget,
currentZone.get().chunkCount()));
}
if (scopedTarget == currentZone.get().otherSet()) {
return placement;
}
return copyPlacement(placement, scopedZone, 1D);
}
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
List<Holder<StructureSet>> structureSets,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (!importedStructures.hasFrequencyOverrides()) {
return structureSets;
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
boolean changed = false;
for (Holder<StructureSet> holder : structureSets) {
Holder<StructureSet> scaled = scaleFrequencyHolder(
holder,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
scaledSets.add(scaled);
changed |= scaled != holder;
}
return changed ? List.copyOf(scaledSets) : structureSets;
}
private static Holder<StructureSet> scaleFrequencyHolder(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
StructureSetKeyIndex keyIndex
) {
if (scaledByIdentity.containsKey(holder)) {
return scaledByIdentity.get(holder);
}
if (!dependsOnFrequencyOverride(
holder, importedStructures, holdersByKey, keyIndex)) {
scaledByIdentity.put(holder, holder);
return holder;
}
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
new IllegalStateException(
"An affected native structure-set exclusion graph has an unkeyed holder"));
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
scaledByIdentity.put(holder, scaledHolder);
StructureSet originalSet = holder.value();
StructurePlacement originalPlacement = originalSet.placement();
Optional<StructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
Optional<StructurePlacement.ExclusionZone> scaledZone = originalZone;
if (originalZone.isPresent()) {
Holder<StructureSet> target = canonicalHolder(
originalZone.get().otherSet(), holdersByKey, keyIndex);
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
target,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
if (scaledTarget != originalZone.get().otherSet()) {
scaledZone = Optional.of(new StructurePlacement.ExclusionZone(
scaledTarget, originalZone.get().chunkCount()));
}
}
double multiplier = importedStructures.frequencyMultiplier(
structureSetKey(holder, keyIndex));
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
? originalPlacement
: copyPlacement(originalPlacement, scaledZone, multiplier);
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
return scaledHolder;
}
private static boolean dependsOnFrequencyOverride(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Holder<StructureSet> current = holder;
while (visited.add(current)) {
if (importedStructures.frequencyMultiplier(
structureSetKey(current, keyIndex)) != 1D) {
return true;
}
Optional<StructurePlacement.ExclusionZone> zone =
exclusionZone(current.value().placement());
if (zone.isEmpty()) {
return false;
}
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
}
return false;
}
private static Holder<StructureSet> canonicalHolder(
Holder<StructureSet> holder,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
String key = structureSetKey(holder, keyIndex);
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
}
private static Holder<StructureSet> replacementHolder(
Holder<StructureSet> original,
StructureSet replacement,
StructureSetKeyIndex keyIndex
) {
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
if (key.isEmpty()) {
return Holder.direct(replacement);
}
ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get());
holder.bind(replacement);
return holder;
}
private static StructurePlacement copyPlacement(
StructurePlacement placement,
Optional<StructurePlacement.ExclusionZone> exclusionZone,
double frequencyMultiplier
) {
Vec3i locateOffset = (Vec3i) declaredFieldValue(
StructurePlacement.class, placement, Vec3i.class);
StructurePlacement.FrequencyReductionMethod frequencyReductionMethod =
(StructurePlacement.FrequencyReductionMethod) declaredFieldValue(
StructurePlacement.class,
placement,
StructurePlacement.FrequencyReductionMethod.class);
float frequency = (float) declaredFieldValue(
StructurePlacement.class, placement, float.class);
int salt = (int) declaredFieldValue(
StructurePlacement.class, placement, int.class);
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
return new RandomSpreadStructurePlacement(
locateOffset,
frequencyReductionMethod,
scale.frequency(),
salt,
exclusionZone,
scale.spacing(),
randomSpread.separation(),
randomSpread.spreadType());
}
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
ConcentricRingsStructurePlacement rings =
(ConcentricRingsStructurePlacement) placement;
float scaledFrequency = NativeStructureFrequencyScale.probability(
frequency, frequencyMultiplier);
return new ConcentricRingsStructurePlacement(
locateOffset,
frequencyReductionMethod,
scaledFrequency,
salt,
exclusionZone,
rings.distance(),
rings.spread(),
rings.count(),
rings.preferredBiomes());
}
throw new IllegalStateException("Unsupported native structure placement in an affected "
+ "frequency-override or datapack-scope graph: "
+ placement.getClass().getName());
}
private static Object declaredFieldValue(
Class<?> declaringType,
Object target,
Class<?> fieldType
) {
Field match = null;
for (Field field : declaringType.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
continue;
}
if (match != null) {
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
+ declaringType.getName());
}
match = field;
}
if (match == null) {
throw new IllegalStateException("Missing " + fieldType.getName() + " field on "
+ declaringType.getName());
}
try {
match.setAccessible(true);
return match.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not read " + fieldType.getName() + " field on "
+ declaringType.getName(), e);
}
}
static final class StructureSetKeyIndex {
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries;
private StructureSetKeyIndex(
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries
) {
this.keysByEntries = keysByEntries;
}
private static StructureSetKeyIndex create(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
new IdentityHashMap<>();
for (Holder<StructureSet> holder : registeredStructureSets) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isEmpty()) {
continue;
}
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
if (previous != null && !previous.equals(key.get())) {
throw new IllegalStateException(
"Multiple registered structure sets share the same entry list");
}
}
return new StructureSetKeyIndex(keys);
}
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key;
}
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
}
}
record Selection(
List<Holder<StructureSet>> structureSets,
int retainedManagedSets,
int excludedManagedSets
) {
}
private static final class ReboundStructureSetHolder extends Holder.Reference<StructureSet> {
private ReboundStructureSetHolder(ResourceKey<StructureSet> key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, key, null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
}
@@ -1,440 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.decoration.IrisDecorationStep;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.FeatureSorter;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
* {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the
* control off nothing here allocates or runs.
*
* <p>Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false},
* so the vanilla feature half never runs on its own. This reproduces that half only - structures are never
* placed twice.
*
* <p>Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The
* FEATURES chunk step is not parallel-safe.
*/
final class ImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine;
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
ImportedFeatureStage(Engine engine) {
this.engine = engine;
}
/**
* Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative.
* Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it
* does today.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return null;
}
Holder<Biome> mapped = table.derivatives().get(holderKey(biome));
return mapped == null ? null : mapped.value().getGenerationSettings();
}
/**
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
*/
void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
synchronized (this) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
runtimeStates.put(runtimeId, build(level, dimension));
}
}
void evictRuntime(int runtimeId) {
runtimeStates.remove(runtimeId);
}
private RuntimeFeatureState currentState() {
IrisDimension dimension = engine.getDimension();
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
return settled(state, dimension) ? state : null;
}
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
return state != null && state.dimension() == dimension;
}
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error);
return new RuntimeFeatureState(dimension, null);
}
if (!control.shouldGenerateFeatures()) {
return new RuntimeFeatureState(dimension, null);
}
FeatureTable built;
try {
built = buildTable(level, control, dimension);
} catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error);
return new RuntimeFeatureState(dimension, null);
}
if (built == null) {
return new RuntimeFeatureState(dimension, null);
}
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps");
return new RuntimeFeatureState(dimension, built);
}
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
IrisDimension dimension) {
Registry<Biome> registry = level.registryAccess().lookupOrThrow(Registries.BIOME);
Set<String> visibleKeys = visibleBiomeKeys();
// Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and
// an unordered walk makes detection depend on JVM hash order.
List<Holder<Biome>> biomes = new ArrayList<>();
Map<String, Holder<Biome>> byKey = new HashMap<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (key != null && visibleKeys.contains(key)) {
biomes.add(reference);
byKey.put(key, reference);
}
});
if (biomes.isEmpty()) {
IrisLogging.error("Iris importedFeatures is on but " + dimensionKey()
+ " exposes no registered biomes; features off");
return null;
}
Map<String, Holder<Biome>> derivatives = customBiomeDerivatives(registry, byKey);
List<FeatureSorter.StepFeatureData> steps;
try {
steps = FeatureSorter.buildFeaturesPerStep(biomes,
(Holder<Biome> biome) -> settingsFor(biome, derivatives).features(), true);
} catch (IllegalStateException error) {
String message = error.getMessage();
if (message == null || !message.contains(CYCLE_MARKER)) {
throw error;
}
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": the registered placed features cannot be ordered. " + message
+ ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false.");
return null;
}
boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty();
return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes),
Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered);
}
/**
* Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every
* scatter entry, and every generated custom biome.
*/
private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addKey(keys, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addKey(keys, scatter);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
}
}
return keys;
}
/**
* Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla
* derivative. The custom biome's own datapack JSON carries no features by design.
*/
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey());
// Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure
// derivative is rewritten away from its vanilla derivative, so the derivative whose features we
// want is not always a biome Iris can emit.
Holder<Biome> derivative = byKey.get(derivativeKey);
if (derivative == null) {
derivative = resolveHolder(registry, derivativeKey);
}
if (derivative == null) {
IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome "
+ irisBiome.getLoadKey()
+ " is not registered; its custom biomes generate no imported features");
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
derivative
);
}
}
return derivatives;
}
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
if (registry == null || key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
.orElse(null);
}
private static BiomeGenerationSettings settingsFor(Holder<Biome> biome,
Map<String, Holder<Biome>> derivatives) {
Holder<Biome> mapped = derivatives.get(holderKey(biome));
return mapped == null
? biome.value().getGenerationSettings()
: mapped.value().getGenerationSettings();
}
/**
* Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return;
}
ChunkPos centerPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<PlacedFeature> featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: NativeStructureWorldgenAccess.create(
level, centerPos,
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue;
}
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex);
}
} catch (Throwable error) {
throw new IllegalStateException("Iris imported feature placement failed for chunk "
+ centerPos.x() + "," + centerPos.z(), error);
} finally {
level.setCurrentlyGenerating(null);
}
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
BlockPos origin, int stepIndex) {
IntSet stepFeatures = new IntArraySet();
for (Holder<Biome> biome : chunkBiomes) {
List<HolderSet<PlacedFeature>> biomeFeatures = settingsFor(biome, table.derivatives()).features();
if (stepIndex >= biomeFeatures.size()) {
continue;
}
for (Holder<PlacedFeature> feature : biomeFeatures.get(stepIndex)) {
stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value()));
}
}
if (stepFeatures.isEmpty()) {
return;
}
// Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own
// global index, so denying one feature never shifts another.
int[] featureIndices = stepFeatures.toIntArray();
Arrays.sort(featureIndices);
for (int globalIndex : featureIndices) {
PlacedFeature feature = stepData.features().get(globalIndex);
if (table.filtered()) {
Identifier featureId = featureRegistry.getKey(feature);
if (featureId != null && !table.control().shouldGenerate(featureId.toString())) {
continue;
}
}
random.setFeatureSeed(decorationSeed, globalIndex, stepIndex);
level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature));
feature.placeWithBiomeCheck(level, owner, random, origin);
}
}
private static String describeFeature(Registry<PlacedFeature> registry, PlacedFeature feature) {
Identifier id = registry.getKey(feature);
return id == null ? feature.toString() : id.toString();
}
private Set<Holder<Biome>> chunkBiomes(
WorldGenLevel level,
SectionPos sectionPos,
FeatureTable table,
DimensionStackContext stackContext
) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
LevelChunkSection[] sections = neighbour.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (stackContext == null) {
section.getBiomes().getAll(collected::add);
continue;
}
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int quartY = 0; quartY < 4; quartY++) {
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
for (int quartX = 0; quartX < 4; quartX++) {
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
for (int quartZ = 0; quartZ < 4; quartZ++) {
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
blockX, internalY, blockZ);
if (layer.terrainContext().isSelfReferencing()) {
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
}
}
}
}
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
for (Holder<Biome> biome : collected) {
if (table.biomeSet().contains(biome)) {
present.add(biome);
continue;
}
String key = holderKey(biome);
Holder<Biome> canonical = key == null ? null : table.byKey().get(key);
if (canonical != null) {
present.add(canonical);
}
}
return present;
}
private static void addKey(Set<String> keys, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
keys.add(normalized);
}
}
private String dimensionKey() {
IrisDimension dimension = engine.getDimension();
return dimension == null ? "<unbound>" : dimension.getLoadKey();
}
private static String holderKey(Holder<Biome> holder) {
return holder.unwrapKey()
.map(key -> key.identifier().toString().toLowerCase(Locale.ROOT))
.orElse(null);
}
private static String normalizeKey(String key) {
Identifier identifier = key == null ? null : Identifier.tryParse(key);
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control,
List<Holder<Biome>> biomes, Set<Holder<Biome>> biomeSet,
Map<String, Holder<Biome>> byKey,
List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) {
}
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
}
}
@@ -1,142 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
import java.util.Optional;
final class NativeStructurePoiUpdates {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
private final int skipPoiFlag;
NativeStructurePoiUpdates(int skipPoiFlag) {
this.skipPoiFlag = skipPoiFlag;
}
static NativeStructurePoiUpdates create() {
try {
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
if ((flags & skipPoiFlag) != 0) {
return world.setBlock(position, state, flags, limit);
}
ChunkAccess chunk = world.getChunk(position);
if (!(chunk instanceof ProtoChunk)) {
return world.setBlock(position, state, flags, limit);
}
if (!(chunk instanceof ImposterProtoChunk)
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
markSection(chunk, chunk.getSectionIndex(position.getY()));
}
return world.setBlock(position, state, flags | skipPoiFlag, limit);
}
static void reconcile(LevelChunk chunk) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
if (sections == null) {
return;
}
validateSections(chunk, sections);
ServerLevel world = (ServerLevel) chunk.getLevel();
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
PoiManager manager = world.getPoiManager();
for (int word = 0; word < sections.length; word++) {
long remaining = sections[word];
while (remaining != 0) {
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
reconcileSection(chunk, manager, index);
remaining &= remaining - 1;
}
}
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
chunk.markUnsaved();
}
private static void markSection(ChunkAccess chunk, int index) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
int word = index >>> 6;
long bit = 1L << (index & 63);
if (sections == null) {
sections = new long[word + 1];
} else {
validateSections(chunk, sections);
if (word < sections.length && (sections[word] & bit) != 0) {
return;
}
if (word >= sections.length) {
sections = Arrays.copyOf(sections, word + 1);
}
}
sections[word] |= bit;
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
}
private static void validateSections(ChunkAccess chunk, long[] sections) {
int count = chunk.getSectionsCount();
int words = (count + 63) >>> 6;
if (sections.length > words
|| sections.length == words && (count & 63) != 0
&& (sections[words - 1] >>> (count & 63)) != 0) {
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
}
}
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
if (existing.isPresent()) {
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
manager.remove(record.getPos());
}
}
}
LevelChunkSection section = chunk.getSection(index);
if (!section.maybeHas(PoiTypes::hasPoi)) {
return;
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
if (type.isEmpty()) {
continue;
}
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
sectionPosition.minBlockZ() + z);
if (!manager.getType(position).equals(type)) {
manager.add(position.immutable(), type.get());
}
}
}
}
}
}
@@ -1,606 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.level.chunk.EmptyLevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.bukkit.event.entity.CreatureSpawnEvent;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
import java.util.function.Supplier;
final class NativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
private final WorldGenLevel delegate;
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
private final AABB generationBounds;
private final LevelTickAccess<Block> blockTicks;
private final LevelTickAccess<Fluid> fluidTicks;
private final Long2LongOpenHashMap terrainHeights;
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
this.generationCenter = Objects.requireNonNull(
boundary.generationCenter(), "Native structure world access requires a generation center");
this.surfaceFirstFreeY = Objects.requireNonNull(
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
this.generationBounds = new AABB(
generationCenter.getMinBlockX() - 16,
delegate.getMinY(),
generationCenter.getMinBlockZ() - 16,
generationCenter.getMaxBlockX() + 17,
delegate.getMinY() + delegate.getHeight(),
generationCenter.getMaxBlockZ() + 17);
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
this.terrainHeights = new Long2LongOpenHashMap();
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
this.outsideChunks = new Long2ObjectOpenHashMap<>();
}
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
public long getSeed() {
return delegate.getSeed();
}
@Override
public boolean ensureCanWrite(BlockPos position) {
return isWritable(position) && delegate.ensureCanWrite(position);
}
@Override
public void setCurrentlyGenerating(Supplier<String> description) {
delegate.setCurrentlyGenerating(description);
}
@Override
public ServerLevel getLevel() {
return delegate.getLevel();
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
if (isReadable(position)) {
return delegate.getCurrentDifficultyAt(position);
}
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
}
@Override
public long nextSubTickCount() {
return delegate.nextSubTickCount();
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return blockTicks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return fluidTicks;
}
@Override
public LevelData getLevelData() {
return delegate.getLevelData();
}
@Override
public MinecraftServer getServer() {
return delegate.getServer();
}
@Override
public ChunkSource getChunkSource() {
return delegate.getChunkSource();
}
@Override
public RandomSource getRandom() {
return delegate.getRandom();
}
@Override
public void updateNeighborsAt(BlockPos position, Block block) {
if (isWritableNeighbourhood(position)) {
delegate.updateNeighborsAt(position, block);
}
}
@Override
public void neighborShapeChanged(Direction direction, BlockPos position,
BlockPos neighbourPosition, BlockState neighbourState,
int updateFlags, int updateLimit) {
if (isWritable(position) && isWritable(neighbourPosition)) {
delegate.neighborShapeChanged(
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
}
}
@Override
public void playSound(Entity source, BlockPos position, SoundEvent sound,
SoundSource soundSource, float volume, float pitch) {
if (isWritable(position)) {
delegate.playSound(source, position, sound, soundSource, volume, pitch);
}
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z,
double velocityX, double velocityY, double velocityZ) {
if (isWritable(BlockPos.containing(x, y, z))) {
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
}
}
@Override
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
if (isWritable(position)) {
delegate.levelEvent(source, eventId, position, data);
}
}
@Override
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
if (isWritable(BlockPos.containing(position))) {
delegate.gameEvent(event, position, context);
}
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ, status, create);
}
return create ? outsideChunk(chunkX, chunkZ) : null;
}
@Override
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
if (!isInsideGenerationRegion(chunkX, chunkZ)) {
return null;
}
return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ);
}
@Override
public boolean hasChunk(int chunkX, int chunkZ) {
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
}
@Override
public int getSkyDarken() {
return delegate.getSkyDarken();
}
@Override
public BiomeManager getBiomeManager() {
return biomeManager;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
return getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
}
return fallbackBiome;
}
@Override
public boolean isClientSide() {
return delegate.isClientSide();
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public DimensionType dimensionType() {
return delegate.dimensionType();
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getHeight() {
return delegate.getHeight();
}
@Override
public RegistryAccess registryAccess() {
return delegate.registryAccess();
}
@Override
public FeatureFlagSet enabledFeatures() {
return delegate.enabledFeatures();
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
return delegate.environmentAttributes();
}
@Override
public LevelLightEngine getLightEngine() {
return delegate.getLightEngine();
}
@Override
public int getBrightness(LightLayer layer, BlockPos position) {
if (isReadable(position)) {
return delegate.getBrightness(layer, position);
}
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
}
@Override
public int getRawBrightness(BlockPos position, int ambientDarkening) {
if (isReadable(position)) {
return delegate.getRawBrightness(position, ambientDarkening);
}
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
}
@Override
public boolean canSeeSky(BlockPos position) {
if (isReadable(position)) {
return delegate.canSeeSky(position);
}
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
}
@Override
public WorldBorder getWorldBorder() {
return delegate.getWorldBorder();
}
@Override
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunkForCollisions(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isWritable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
public BlockState getBlockState(BlockPos position) {
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null;
}
@Override
public FluidState getFluidState(BlockPos position) {
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getFluidIfLoaded(position) : null;
}
@Override
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
}
@Override
public <T extends Entity> List<T> getEntities(
EntityTypeTest<Entity, T> type,
AABB area, Predicate<? super T> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
}
@Override
public List<? extends Player> players() {
return delegate.players();
}
@Override
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
return predicate.test(getBlockState(position));
}
@Override
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
return predicate.test(getFluidState(position));
}
@Override
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
return position.atY(getHeight(type, position.getX(), position.getZ()));
}
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position)
&& (POI_UPDATES != null
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
: delegate.setBlock(position, state, updateFlags, updateLimit));
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
: delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? !delegate.getBlockState(position).isAir()
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
: delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
public boolean addFreshEntity(Entity entity) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
}
@Override
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason);
}
private boolean isReadable(BlockPos position) {
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
&& !isOutsideBuildHeight(position);
}
private boolean isWritable(BlockPos position) {
return isReadable(position) && !protectedPosition.test(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
return isWritable(position)
&& isWritable(position.north())
&& isWritable(position.south())
&& isWritable(position.east())
&& isWritable(position.west())
&& isWritable(position.above())
&& isWritable(position.below());
}
private AABB boundedArea(AABB area) {
double minX = Math.max(area.minX, generationBounds.minX);
double minY = Math.max(area.minY, generationBounds.minY);
double minZ = Math.max(area.minZ, generationBounds.minZ);
double maxX = Math.min(area.maxX, generationBounds.maxX);
double maxY = Math.min(area.maxY, generationBounds.maxY);
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
return null;
}
if (minX == area.minX && minY == area.minY && minZ == area.minZ
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
return area;
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
}
@Override
public boolean isOutsideBuildHeight(BlockPos position) {
return position.getY() < getMinY() || position.getY() >= getMaxY();
}
private BlockState terrainState(BlockPos position) {
if (isOutsideBuildHeight(position)) {
return Blocks.VOID_AIR.defaultBlockState();
}
long heights = heights(position.getX(), position.getZ());
int surface = (int) (heights >> 32);
int floor = (int) heights;
if (position.getY() < floor) {
return Blocks.STONE.defaultBlockState();
}
if (position.getY() < surface) {
return Blocks.WATER.defaultBlockState();
}
return Blocks.AIR.defaultBlockState();
}
private int height(Heightmap.Types type, int x, int z) {
long heights = heights(x, z);
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
return (int) heights;
}
return (int) (heights >> 32);
}
private long heights(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
long cached = terrainHeights.get(key);
if (cached != Long.MIN_VALUE) {
return cached;
}
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
terrainHeights.put(key, heights);
return heights;
}
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
long key = ChunkPos.pack(chunkX, chunkZ);
ChunkAccess cached = outsideChunks.get(key);
if (cached != null) {
return cached;
}
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
outsideChunks.put(key, chunk);
return chunk;
}
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
private final LevelTickAccess<T> delegate;
private final Predicate<BlockPos> writable;
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
this.delegate = delegate;
this.writable = writable;
}
@Override
public void schedule(ScheduledTick<T> tick) {
if (writable.test(tick.pos())) {
delegate.schedule(tick);
}
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return writable.test(position) && delegate.hasScheduledTick(position, type);
}
@Override
public int count() {
return delegate.count();
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return writable.test(position) && delegate.willTickThisTick(position, type);
}
}
}
@@ -1,61 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.agent.Agent;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import java.util.function.LongPredicate;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import java.util.List;
final class NmsGenerationHooks {
private NmsGenerationHooks() {
}
static void install() {
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
.make()
.load(clazz.getClassLoader(), Agent.installed());
}
buddy.redefine(WorldGenRegion.class)
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
.and(ElementMatchers.takesArguments(BlockPos.class))))
.make()
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
}
private static class WorldGenerationWriteAdvice {
@Advice.OnMethodExit
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
@Advice.Return(readOnly = false) boolean allowed) {
if (!allowed) {
return;
}
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
if (generator instanceof LongPredicate predicate
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator")
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
}
}
}
private static class ChunkAccessAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
return index >= access.getPostProcessing().length;
}
}
}
@@ -1,182 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Supplier;
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
private static final String BLOCK_REGISTRY = "minecraft:block";
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
private final Supplier<RegistryAccess> registryAccess;
private final String runtimeIdentity;
private final String rendererIdentity;
NmsGenerationRegistry(
Supplier<RegistryAccess> registryAccess,
String runtimeIdentity,
String rendererIdentity
) {
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
case DIMENSION_TYPE_REGISTRY -> presence(
access,
Registries.DIMENSION_TYPE,
registryKey,
resourceKey
);
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
};
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
default -> throw new IllegalArgumentException(
"Registry does not accept generated JSON definitions: " + registryKey
);
};
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> exactRegistered(
access,
Registries.BIOME,
Biome.DIRECT_CODEC,
resourceKey
);
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
access,
Registries.DIMENSION_TYPE,
DimensionType.DIRECT_CODEC,
resourceKey
);
default -> throw new IllegalArgumentException(
"Registry does not contain Iris-generated definitions: " + registryKey
);
};
}
private static <T> Definition presence(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
String registryResourceKey,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
+ " / " + resourceKey + ".");
}
return Definition.resourceIdentity(registryResourceKey, resourceKey);
}
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static <T> Definition exactRegistered(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
Codec<T> codec,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing generated registry definition "
+ registryKey.identifier() + " / " + resourceKey + ".");
}
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static Identifier requireIdentifier(String resourceKey) {
Identifier identifier = Identifier.tryParse(resourceKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
}
return identifier;
}
private static String requireText(String value, String label) {
String required = Objects.requireNonNull(value, label).trim();
if (required.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return required;
}
}
@@ -1,266 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.LockSupport;
final class NmsSavedTerrainCapture {
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
private static final int SNAPSHOT_BATCH_SIZE = 64;
private NmsSavedTerrainCapture() {
}
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
ServerLevel level = ((CraftWorld) world).getHandle();
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
J.runRegionFuture(world, chunkX, chunkZ, () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
if (chunk == null) {
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
return;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
}
snapshot.complete(scheduleSnapshot(level, chunk));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
return snapshot.thenApplyAsync(captured -> {
try {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
CompoundTag data = awaitSnapshot(level, captured, deadline);
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
awaitWrite(level, checkpoint.chunk(), deadline);
if (data == null) {
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
}
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
NbtIo.write(data, output);
}
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static CompletableFuture<Void> flush(World world) {
ServerLevel level = ((CraftWorld) world).getHandle();
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
return CompletableFuture.runAsync(() -> {
try {
flushSnapshots(world, level, holders);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
awaitSnapshots(snapshots, deadline);
for (CompletableFuture<CapturedChunk> future : snapshots) {
CapturedChunk captured = future.join();
CompoundTag data = awaitSnapshot(level, captured, deadline);
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
}
for (int index = offset; index < end; index++) {
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
}
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
}
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
World world, ServerLevel level, List<NewChunkHolder> holders) {
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
for (NewChunkHolder original : holders) {
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
snapshots.add(snapshot);
J.runRegionFuture(world, position.x(), position.z(), () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
.getChunkHolder(position.x(), position.z());
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
}
return snapshots;
}
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
throws IOException {
try {
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
}
}
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
for (Checkpoint checkpoint : checkpoints) {
awaitWrite(level, checkpoint.chunk(), deadline);
}
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
for (Checkpoint checkpoint : checkpoints) {
verifyCheckpoint(worldFolder, checkpoint);
}
}
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
if (data == null) {
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
}
byte[] receipt = data.getCompound("ChunkBukkitValues")
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
data.getStringOr("Status", ""), receipt, structureActivation));
}
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
Verification expected = checkpoint.verification();
if (expected == null) {
return;
}
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
expected.status(), expected.receipt(), expected.structureActivation());
}
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (true) {
checkWriteWait(chunk, deadlineNanos);
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
return;
}
helpSaveTasks(scheduler);
}
}
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
Completable<CompoundTag> nativeData = new Completable<>();
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
task.queue();
ChunkPos position = chunk.getPos();
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
return new CapturedChunk(position, data);
}
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (!captured.data().isDone()) {
checkWriteWait(captured.chunk(), deadlineNanos);
helpSaveTasks(scheduler);
}
try {
return captured.data().join();
} catch (CompletionException failure) {
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
}
}
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
CompletableFuture<CompoundTag> data) {
try {
CompoundTag serialized = snapshot.write();
nativeData.complete(serialized);
data.complete(serialized);
} catch (Throwable failure) {
nativeData.completeExceptionally(failure);
data.completeExceptionally(failure);
}
}
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
}
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
}
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
}
record Checkpoint(ChunkPos chunk, Verification verification) {
}
record Verification(String status, byte[] receipt, long structureActivation) {
}
}
@@ -1,403 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.agent.Agent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
final class NmsWorldLifecycle {
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer;
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
private boolean pluginClassLoaderCloseDeferred;
public boolean injectBukkit() {
synchronized (injected) {
if (injected.get()) {
return true;
}
try {
IrisLogging.info("Injecting Bukkit");
Agent.requireClassLoaderCloseDeferral();
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
.getMethod("close").getDeclaringClass();
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
pluginClassLoaderTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
.with(loaderListener)
.type(ElementMatchers.is(loaderCloseType))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
.on(ElementMatchers.named("close")
.and(ElementMatchers.takesArguments(0))
.and(ElementMatchers.returns(void.class)))))
.installOn(Agent.getInstrumentation());
loaderListener.requireInstalled();
levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArguments(4))
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
.and(ElementMatchers.takesArgument(1, String.class))
.and(ElementMatchers.takesArgument(2, Path.class))
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
.installOn(Agent.getInstrumentation());
serverLevelTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
NmsGenerationHooks.install();
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
pluginClassLoaderTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel,
partialStorageAccess,
partialPluginClassLoader
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
}
}
return false;
}
}
}
public void ensureServerLevelInjection() {
if (!injected.get()) {
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
}
try {
Agent.getInstrumentation().retransformClasses(
LevelStorageSource.LevelStorageAccess.class,
ServerLevel.class
);
} catch (Throwable e) {
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
}
}
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
injected.set(false);
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
activeServerLevel,
activeStorageAccess
}) {
if (transformer == null) {
continue;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
}
}
}
}
public boolean isServerStopping() {
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
}
public ServerShutdownBoundary createServerShutdownBoundary() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
return new ServerShutdownBoundary(
() -> server.hasFullyShutdown,
server.getRunningThread()
);
}
public void deferPluginClassLoaderClose() {
synchronized (injected) {
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
return;
}
Agent.retainClassLoader(getClass().getClassLoader());
pluginClassLoaderCloseDeferred = true;
}
}
public void releasePluginClassLoaderClose() {
ResettableClassFileTransformer transformer;
boolean releaseLoader;
synchronized (injected) {
transformer = pluginClassLoaderTransformer;
pluginClassLoaderTransformer = null;
releaseLoader = pluginClassLoaderCloseDeferred;
pluginClassLoaderCloseDeferred = false;
}
try {
if (transformer != null
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
}
} finally {
if (releaseLoader) {
Agent.releaseClassLoader(getClass().getClassLoader());
}
}
}
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
private volatile boolean transformed;
private volatile Throwable failure;
@Override
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
boolean loaded, DynamicType dynamicType) {
transformed = true;
}
@Override
public void onError(String typeName, ClassLoader loader, JavaModule module,
boolean loaded, Throwable throwable) {
failure = throwable;
}
private void requireInstalled() {
if (!transformed || failure != null) {
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
}
}
}
private static class PluginClassLoaderCloseAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
Class<?> installer = Class.forName(
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
.invoke(null, loader));
}
}
private static class LevelStorageAccessAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(1) String levelId,
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
) {
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
return;
}
Object generator;
try {
generator = stagingType
.getDeclaredMethod("peekStemGenerator", String.class)
.invoke(null, levelId);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to inspect the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (generator == null || !generatorType.isInstance(generator)) {
return;
}
try {
Object target = generatorType.getMethod("getTarget").invoke(generator);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to bind the staged world storage identity",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
private static class ServerLevelAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.AllArguments Object[] constructorArguments
) {
if (dimensionKey == null)
return;
// This advice is inlined into every ServerLevel construction on the server. Until a
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
// Iris being unloaded or half-loaded must never break other plugins' world creation.
String levelId;
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
return;
}
// From here Iris is present and its classes resolve; a failure resolving the
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
ChunkGenerator constructorGenerator = null;
for (Object argument : constructorArguments) {
if (argument instanceof ChunkGenerator candidate) {
constructorGenerator = candidate;
break;
}
}
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = stagingType
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
gen = owned;
}
} catch (Throwable e) {
throw new RuntimeException("Iris failed to resolve the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (gen == null) {
return;
}
// Past the ownership gate the world is Iris-owned; silently handing back the
// vanilla stem would corrupt generation, so failures from here rethrow.
try {
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
if (bindings == null) {
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
}
Method stemMethod = null;
for (Method candidate : bindings.getClass().getMethods()) {
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
stemMethod = candidate;
break;
}
}
if (stemMethod == null) {
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
}
Object target = generatorType.getMethod("getTarget").invoke(gen);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
}
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
levelStem = runtimeStem;
} catch (Throwable e) {
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
}
@@ -1,80 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
final class VanillaStructureBiomes {
private VanillaStructureBiomes() {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
if (source == null) {
throw new IllegalStateException("Minecraft chunk generator has no biome source");
}
Set<String> keys = new LinkedHashSet<>();
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
? customBiomeSource.possibleStructureBiomes()
: source.possibleBiomes();
for (Holder<Biome> holder : possibleBiomes) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
if (structureKey == null || structureKey.isBlank()) {
throw new IllegalArgumentException("Registered structure key must not be blank");
}
Set<String> keys = new LinkedHashSet<>();
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
boolean hasFilterEntries = false;
for (Holder<Biome> holder : structure.biomes()) {
hasFilterEntries = true;
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
// An empty biome filter is legal datapack content (opt-in structures whose tags only
// reference absent modded biomes); the structure is unreachable, not an error state.
if (keys.isEmpty() && hasFilterEntries) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' has biome filter entries but none resolve to registered biome keys");
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
if (level == null) {
throw new IllegalStateException("Minecraft server level is unavailable");
}
Set<String> possible = possibleBiomeKeys(source);
return NativeStructureGenerationKeys.reachable(level, possible);
}
}
@@ -1,12 +1,15 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainColumns;
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
@@ -21,7 +24,7 @@ import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
@@ -29,6 +32,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
public final class IrisChunkGeneratorHeightCacheTest {
@Rule
@@ -41,73 +45,76 @@ public final class IrisChunkGeneratorHeightCacheTest {
}
@Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
}
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
}
@Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
verify(fixture.engine(), times(1)).getHeight(0, 0, true);
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
}
@Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture();
NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
when(policy.openQuery(any())).thenReturn(session);
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null));
}
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
verify(session, times(6)).resolvedColumn();
}
private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "engine", engine);
set(generator, "terrainHeights", new NativeTerrainHeightCache());
return new Fixture(generator, engine, complex);
return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
}
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
return generator;
}
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
Field field = NativeChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
@@ -125,6 +132,6 @@ public final class IrisChunkGeneratorHeightCacheTest {
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
}
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
}
}
@@ -1,5 +1,9 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeGenerationWriteGuard;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.IrisComplex;
@@ -45,11 +49,11 @@ public final class NativeGenerationWriteGuardTest {
DimensionStackContext stack = mock(DimensionStackContext.class);
when(objects.contains(-3, 74, -7)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, stack, -64));
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
verifyNoInteractions(stack);
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
assertFalse(NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, null, -64))
.test(new BlockPos(-3, 11, -7)));
}
@@ -65,7 +69,7 @@ public final class NativeGenerationWriteGuardTest {
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, stack, -64));
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
@@ -85,10 +89,10 @@ public final class NativeGenerationWriteGuardTest {
when(router.history()).thenReturn(history);
when(history.activeActivation()).thenReturn(active);
when(active.activationId()).thenReturn(4L);
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
assertTrue(GenerationWritePolicy.isHistoricalStructure(engine, 3));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 0));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 4));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 5));
}
@Test
@@ -100,11 +104,11 @@ public final class NativeGenerationWriteGuardTest {
ChunkAccess partial = mock(ChunkAccess.class);
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine.getComplex()::allowsMantleChunkWrite, region, new ChunkPos(0, 0)));
ChunkAccess complete = mock(ChunkAccess.class);
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
when(region.getChunk(1, -1)).thenReturn(complete);
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine.getComplex()::allowsMantleChunkWrite, region, new ChunkPos(0, 0)));
}
@Test
@@ -115,11 +119,11 @@ public final class NativeGenerationWriteGuardTest {
ChunkAccess chunk = mock(ChunkAccess.class);
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE));
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.NOISE));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.FEATURES));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.SPAWN));
}
}
@@ -1,5 +1,19 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureStartInjector;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureFactory;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeJigsawMetadata;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.ForcedStructureChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTemplatePoolBounds;
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import com.mojang.datafixers.util.Either;
@@ -108,10 +122,10 @@ public class NativeStructureFactoryTest {
distance.putInt("vertical", 64);
axisSpecific.put("max_distance_from_center", distance);
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal"));
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical"));
assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal"));
assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical"));
assertEquals(80, NativeJigsawMetadata.sourceDistance(scalar, "horizontal"));
assertEquals(80, NativeJigsawMetadata.sourceDistance(scalar, "vertical"));
assertEquals(96, NativeJigsawMetadata.sourceDistance(axisSpecific, "horizontal"));
assertEquals(64, NativeJigsawMetadata.sourceDistance(axisSpecific, "vertical"));
}
@Test
@@ -121,8 +135,8 @@ public class NativeStructureFactoryTest {
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
.build());
assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none));
assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox));
assertEquals(0, NativeJigsawMetadata.horizontalReferenceExpansion(none));
assertEquals(12, NativeJigsawMetadata.horizontalReferenceExpansion(beardBox));
}
@Test
@@ -1,5 +1,7 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureGenerationKeys;
import net.minecraft.SharedConstants;
import net.minecraft.core.Vec3i;
import net.minecraft.server.Bootstrap;
@@ -1,5 +1,7 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureLocatePersistence;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
@@ -1,11 +1,19 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceRepair;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureOwnershipFingerprint;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -65,7 +73,7 @@ public class NativeStructureOwnershipFingerprintTest {
generated, 0, 80, -64, 320);
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
@@ -85,11 +93,9 @@ public class NativeStructureOwnershipFingerprintTest {
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", reloaded);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", wrapped, NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)), plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()));
assertEquals(beforeReload, afterReload);
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
@@ -110,11 +116,9 @@ public class NativeStructureOwnershipFingerprintTest {
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
expected,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()),
expected.getBoundingBox());
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", expected, expected.getBoundingBox()), plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()));
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
}
@@ -129,18 +133,16 @@ public class NativeStructureOwnershipFingerprintTest {
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", wrapped, NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)), plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()));
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(wrapped.getPieces()));
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
@@ -263,13 +265,10 @@ public class NativeStructureOwnershipFingerprintTest {
private static NativeStructureOwnershipRecord ownership(
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
return NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
start,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()),
NativeStructureReferenceEnvelope.referenceBounds(
start, structure, new IrisStructureTerrain())
);
return NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", start, NativeStructureReferenceEnvelope.referenceBounds(
start, structure, new IrisStructureTerrain())), plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()));
}
private static StructureStart poolStart(
@@ -1,12 +1,22 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.terrain.structure.StructureOwnershipRecordView;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureOwnershipRecovery;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureOwnershipFingerprint;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
@@ -63,7 +73,7 @@ public class NativeStructureOwnershipRecoveryTest {
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, reloaded,
plan, expected, false);
plan, expected, false, NativeStructureOwnershipRecord::capture);
assertNotNull(recovered);
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
@@ -71,7 +81,7 @@ public class NativeStructureOwnershipRecoveryTest {
assertEquals(expectedLocatorY, recovered.locatorY());
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
recovered.locatorY());
assertEquals(IrisStructureTerrainMode.FORCE_CARVE,
assertEquals(StructureTerrainMode.FORCE_CARVE,
recovered.restoredDecision().terrain().resolvedMode());
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
assertTrue(recovered.referenceMinChunkX()
@@ -89,7 +99,7 @@ public class NativeStructureOwnershipRecoveryTest {
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, natural,
plan, natural, true);
plan, natural, true, NativeStructureOwnershipRecord::capture);
assertNull(recovered);
}
@@ -114,10 +124,10 @@ public class NativeStructureOwnershipRecoveryTest {
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
changedIdentity, expected, false));
changedIdentity, expected, false, NativeStructureOwnershipRecord::capture));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, changedGeometry,
plan(origin, "geometry-check", 0), expected, false));
plan(origin, "geometry-check", 0), expected, false, NativeStructureOwnershipRecord::capture));
}
@Test
@@ -129,7 +139,7 @@ public class NativeStructureOwnershipRecoveryTest {
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
plan(origin, "fingerprint-check", 16), expected, false);
plan(origin, "fingerprint-check", 16), expected, false, NativeStructureOwnershipRecord::capture);
assertNotNull(recovered);
StructureStart moved = monumentStart(structure, origin, seed);
@@ -148,10 +158,11 @@ public class NativeStructureOwnershipRecoveryTest {
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0);
origin, "vacuum-envelope-refresh", StructureTerrainMode.VACUUM, 0);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, content);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, content), plan);
NativeStructureOwnershipRecord refreshed =
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
@@ -166,7 +177,7 @@ public class NativeStructureOwnershipRecoveryTest {
assertEquals(stale.locatorY(), refreshed.locatorY());
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
assertEquals(stale.decision(), refreshed.decision());
assertEquals(IrisStructureTerrainMode.VACUUM,
assertEquals(StructureTerrainMode.VACUUM,
refreshed.restoredDecision().terrain().resolvedMode());
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
@@ -187,10 +198,10 @@ public class NativeStructureOwnershipRecoveryTest {
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, expected, plan,
NativeStructureReferenceEnvelope.contentBounds(expected));
origin, "vacuum-content-check", StructureTerrainMode.VACUUM, 0);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, expected, NativeStructureReferenceEnvelope.contentBounds(expected)), plan);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
@@ -210,8 +221,9 @@ public class NativeStructureOwnershipRecoveryTest {
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, envelope), plan);
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
@@ -226,19 +238,20 @@ public class NativeStructureOwnershipRecoveryTest {
StructureStart start = monumentStart(structure, origin, seed);
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
int maximumReferenceBlockX = ((origin.x()
+ NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
+ StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
int shiftX = maximumReferenceBlockX - initial.maxX();
for (StructurePiece piece : start.getPieces()) {
piece.move(shiftX, 0, 0);
}
NativeStructureStartPlan plan = plan(
origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0);
origin, "clipped-vacuum", StructureTerrainMode.VACUUM, 0);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, envelope), plan);
assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS,
assertEquals(origin.x() + StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS,
ownership.referenceMaxChunkX());
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
@@ -270,12 +283,12 @@ public class NativeStructureOwnershipRecoveryTest {
String placementId,
int horizontalPadding) {
return plan(origin, placementId,
IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding);
StructureTerrainMode.FORCE_CARVE, horizontalPadding);
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
IrisStructureTerrainMode terrainMode,
StructureTerrainMode terrainMode,
int horizontalPadding) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
@@ -1,8 +1,18 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.terrain.structure.StructurePalette;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.iris.structure.placement.IrisStructureYBand;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
@@ -76,7 +86,7 @@ public class NativeStructurePostProcessorEncaseTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(1)
.setCeilingPadding(1)
.setFloorPadding(1),
@@ -100,7 +110,7 @@ public class NativeStructurePostProcessorEncaseTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
new IrisStructureTerrain().setMode(StructureTerrainMode.ENCASE),
null);
assertEquals(-1, bounds.minY());
@@ -181,7 +191,7 @@ public class NativeStructurePostProcessorEncaseTest {
world(new HashMap<>()), area,
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
"test:bounded-snapshot", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE))));
assertNotNull(sourceTerrain);
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
@@ -210,7 +220,7 @@ public class NativeStructurePostProcessorEncaseTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setMode(StructureTerrainMode.ENCASE)
.setEncasePalette(palette),
NativeStructurePostProcessorEncaseTest::tuffBlock);
@@ -221,10 +231,10 @@ public class NativeStructurePostProcessorEncaseTest {
@Test
public void omittedTerrainConfigurationRetainsSourceSemantics() {
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
StructureTerrainSettings resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode());
assertEquals(StructureTerrainMode.SOURCE, resolved.resolvedMode());
}
}
@@ -240,7 +250,7 @@ public class NativeStructurePostProcessorEncaseTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:bury", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
@@ -267,7 +277,7 @@ public class NativeStructurePostProcessorEncaseTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:encapsulate", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
@@ -304,7 +314,7 @@ public class NativeStructurePostProcessorEncaseTest {
@Test
public void explicitTerrainConfigurationWinsOverAutoEncase() {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
.setMode(StructureTerrainMode.PRESERVE);
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
@@ -313,11 +323,11 @@ public class NativeStructurePostProcessorEncaseTest {
@Test
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
IrisStructureTerrain source = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.SOURCE);
.setMode(StructureTerrainMode.SOURCE);
IrisStructureTerrain preserve = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
.setMode(StructureTerrainMode.PRESERVE);
IrisStructureTerrain encase = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE);
.setMode(StructureTerrainMode.ENCASE);
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), source));
@@ -337,7 +347,7 @@ public class NativeStructurePostProcessorEncaseTest {
BoundingBox sourceBounds = generated.getBoundingBox();
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(4);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
@@ -373,7 +383,7 @@ public class NativeStructurePostProcessorEncaseTest {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(112);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
@@ -446,7 +456,7 @@ public class NativeStructurePostProcessorEncaseTest {
assertEquals(minY, start.getBoundingBox().minY());
}
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
private static BlockState tuffBlock(StructurePalette palette, RNG rng, int x, int y, int z) {
assertNotNull(palette);
assertNotNull(rng);
return Blocks.TUFF.defaultBlockState();
@@ -1,7 +1,11 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
@@ -95,7 +99,7 @@ public class NativeStructurePostProcessorMonumentTest {
start.getStructure(),
0,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(4));
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
@@ -1,5 +1,11 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureSurfaceFitter;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReflection;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
@@ -1,9 +1,29 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.generation.mantle.StructureCarvingFootprint;
import art.arcane.iris.structure.placement.IrisStructureCarveShape;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureFoundationBuilder;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureSurfaceSupportBuilder;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureSurfaceFitter;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVegetationClearer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTemplateOccupancy;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReflection;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.WorldgenTerrainHeightmaps;
import art.arcane.volmlib.util.structure.StructureCarvingFootprint;
import art.arcane.volmlib.util.structure.StructureCarveShape;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
@@ -214,10 +234,10 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, x, sourceY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
assertTrue(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
surfaceTarget(start, StructureTerrainMode.FLATTEN)));
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)),
(x, z) -> x < 2 ? groundY - 32 : groundY + 32);
for (int x = 0; x <= 3; x++) {
@@ -240,7 +260,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, groundY - 19, 0, Blocks.DIRT.defaultBlockState());
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY);
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 19; y <= groundY; y++) {
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, y, 0));
@@ -260,9 +280,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> originalSubmerged = new HashMap<>(submerged);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(buried), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> 100);
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> 100);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(submerged), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8);
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8);
assertEquals(originalBuried, buried);
assertEquals(originalSubmerged, submerged);
@@ -280,12 +300,12 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = new StructureStart(structure, new ChunkPos(0, 0), 0,
new PiecesContainer(desertStart().getPieces()));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
surfaceTarget(start, StructureTerrainMode.FLATTEN)));
}
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.BURY), IrisStructureTerrainMode.FLATTEN)));
surfaceTarget(desertStart(TerrainAdjustment.BURY), StructureTerrainMode.FLATTEN)));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), IrisStructureTerrainMode.FLATTEN)));
surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), StructureTerrainMode.FLATTEN)));
}
@Test
@@ -294,8 +314,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 128, 0, 0, 136, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setFlattenRange(32);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 0, 255, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 94, 0, Blocks.GOLD_BLOCK.defaultBlockState());
@@ -323,7 +343,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(StructureTerrainMode.FLATTEN)
.setHorizontalPadding(128).setFlattenRange(128);
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(start, start.getStructure(), terrain);
@@ -351,7 +371,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written);
@@ -372,7 +392,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, 80, 0, Blocks.STONE.defaultBlockState());
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertTrue(written.isEmpty());
@@ -399,7 +419,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)),
(x, z) -> x == footprint.minX() ? 80 : 112);
assertEquals(81, piece.getBoundingBox().minY());
@@ -421,8 +441,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 4, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 128, 0);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 96);
for (int x : List.of(0, 8)) {
@@ -472,8 +492,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(0, 65, 0, 8, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
@@ -532,8 +552,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 6, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24).setFlattenRange(32);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
@@ -579,8 +599,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
}
}
StructureStart start = rigidSurfaceStart(pieces);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24).setFlattenRange(64);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 8);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = 0; x <= 8; x++) {
@@ -667,11 +687,11 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(
new NativeStructureTerrainIntegrator.TerrainTarget(
"test:beard_box", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE))));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(
new NativeStructureTerrainIntegrator.TerrainTarget(
"test:beard_box", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.PRESERVE))));
new IrisStructureTerrain().setMode(StructureTerrainMode.PRESERVE))));
}
@Test
@@ -681,15 +701,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.TerrainTarget sourceNone =
new NativeStructureTerrainIntegrator.TerrainTarget(
"test:none", none,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE));
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE));
NativeStructureTerrainIntegrator.TerrainTarget vacuumNone =
new NativeStructureTerrainIntegrator.TerrainTarget(
"test:none", none,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM));
new IrisStructureTerrain().setMode(StructureTerrainMode.VACUUM));
NativeStructureTerrainIntegrator.TerrainTarget vacuumBox =
new NativeStructureTerrainIntegrator.TerrainTarget(
"test:box", box,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM));
new IrisStructureTerrain().setMode(StructureTerrainMode.VACUUM));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(sourceNone));
assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumNone));
@@ -778,7 +798,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget(
"nova_structures:tavern_oak", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)));
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE)));
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area, targets,
(x, z) -> 65);
@@ -886,7 +906,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 64, 0));
@@ -911,7 +931,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0));
@@ -932,7 +952,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 80);
for (int y = 61; y <= 74; y++) {
@@ -957,7 +977,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 79);
for (int y = 61; y <= 74; y++) {
@@ -989,7 +1009,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
for (int y = 61; y <= 64; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
@@ -1029,7 +1049,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
@@ -1051,7 +1071,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BoundingBox(15, 65, 0, 16, 70, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
NativeStructureTerrainIntegrator.TerrainTarget target =
surfaceTarget(start, IrisStructureTerrainMode.VACUUM);
surfaceTarget(start, StructureTerrainMode.VACUUM);
BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 72, 15);
BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 72, 15);
BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 72, 15);
@@ -1100,7 +1120,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
@@ -1121,7 +1141,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
@@ -1145,7 +1165,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 66, 0));
@@ -1170,7 +1190,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
@@ -1190,7 +1210,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0));
@@ -1215,7 +1235,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(originalBlocks, blocks);
@@ -1236,7 +1256,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 4, 61, 4));
@@ -1254,7 +1274,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 50);
assertEquals(originalBlocks, blocks);
@@ -1273,7 +1293,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60, 1);
assertEquals(originalBlocks, blocks);
@@ -1297,7 +1317,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(start, StructureTerrainMode.VACUUM)),
(x, z) -> 60);
for (int y = 61; y <= 65; y++) {
@@ -1317,7 +1337,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BoundingBox(15, 66, 0, 16, 72, 0), 1, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE);
NativeStructureTerrainIntegrator.TerrainTarget target =
surfaceTarget(start, IrisStructureTerrainMode.VACUUM);
surfaceTarget(start, StructureTerrainMode.VACUUM);
BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 74, 15);
BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 74, 15);
BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 74, 15);
@@ -1573,13 +1593,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Set<Long> vacuumWritten = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
world(vacuumBlocks), area,
List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(vacuumStart, StructureTerrainMode.VACUUM)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), vacuumWritten);
assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
world(supportTerrain(0, 0)), area,
List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.SOURCE)),
List.of(surfaceTarget(vacuumStart, StructureTerrainMode.SOURCE)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty());
PoolElementStructurePiece distantUpper = rigidTemplatePiece(
@@ -1589,7 +1609,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
List.of(lower, distantUpper), TerrainAdjustment.NONE);
assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
world(supportTerrain(0, 0)), area,
List.of(surfaceTarget(distantStart, IrisStructureTerrainMode.VACUUM)),
List.of(surfaceTarget(distantStart, StructureTerrainMode.VACUUM)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty());
}
@@ -1676,8 +1696,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
world(blocks), area, List.of(
surfaceTarget(start, IrisStructureTerrainMode.PRESERVE),
surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
surfaceTarget(start, StructureTerrainMode.PRESERVE),
surfaceTarget(start, StructureTerrainMode.VACUUM)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written);
@@ -1875,6 +1895,72 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0));
}
@Test
public void loweredSnowSurfaceRemovesSolidSlabDecoration() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = 82; y < 99; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
put(blocks, 0, 99, 0, Blocks.SNOW_BLOCK.defaultBlockState());
put(blocks, 0, 100, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 99, 83, -64, 319);
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 82, 0));
assertEquals(Blocks.SNOW_BLOCK.defaultBlockState(), state(blocks, 0, 83, 0));
for (int y = 84; y <= 100; y++) {
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void loweredSurfaceRemovesStackedSolidDecorationUntilAir() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState());
put(blocks, 0, 66, 0, Blocks.COBBLESTONE_STAIRS.defaultBlockState());
put(blocks, 0, 67, 0, Blocks.COBBLESTONE_WALL.defaultBlockState());
put(blocks, 0, 69, 0, Blocks.STONE.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0));
for (int y = 63; y <= 68; y++) {
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 69, 0));
}
@Test
public void loweredSurfacePreservesTreeDecorationBoundary() {
for (BlockState tree : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState());
put(blocks, 0, 66, 0, tree);
put(blocks, 0, 67, 0, Blocks.STONE.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0));
for (int y = 63; y <= 65; y++) {
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
assertEquals(tree, state(blocks, 0, 66, 0));
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 67, 0));
}
}
@Test
public void rigidSurfaceBaseClosesAOneBlockSubsurfaceGap() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
@@ -2033,7 +2119,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:desert_pyramid", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null);
new IrisStructureTerrain().setMode(StructureTerrainMode.VACUUM), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
@@ -2055,7 +2141,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:desert_pyramid", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(1)
.setCeilingPadding(1), null);
@@ -2089,7 +2175,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null);
new IrisStructureTerrain().setMode(StructureTerrainMode.FORCE_CARVE), null);
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, firstBounds.minX(), y, z));
@@ -2338,8 +2424,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setShape(IrisStructureCarveShape.ERODED)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setShape(StructureCarveShape.ERODED)
.setHorizontalPadding(6)
.setCeilingPadding(8)
.setFloorPadding(0)
@@ -2378,7 +2464,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart generated = desertStart();
BoundingBox content = generated.getBoundingBox();
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
@@ -2403,7 +2489,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart generated = desertStart(TerrainAdjustment.NONE);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.VACUUM)
.setMode(StructureTerrainMode.VACUUM)
.setHorizontalPadding(64);
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(
@@ -2425,7 +2511,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
ChunkPos origin = generated.getChunkPos();
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(124);
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(
@@ -2575,11 +2661,11 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget(
StructureStart start) {
return surfaceTarget(start, IrisStructureTerrainMode.SOURCE);
return surfaceTarget(start, StructureTerrainMode.SOURCE);
}
private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget(
StructureStart start, IrisStructureTerrainMode mode) {
StructureStart start, StructureTerrainMode mode) {
return new NativeStructureTerrainIntegrator.TerrainTarget(
"nova_structures:tavern_oak", start,
new IrisStructureTerrain().setMode(mode));
@@ -2619,8 +2705,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve(
StructureStart start, int horizontalPadding, double lobeStrength) {
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setShape(IrisStructureCarveShape.ERODED)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setShape(StructureCarveShape.ERODED)
.setHorizontalPadding(horizontalPadding)
.setCeilingPadding(8)
.setFloorPadding(0)
@@ -2630,7 +2716,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
return NativeStructureTerrainIntegrator.organicCarve(
NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager),
terrain, IrisStructureCarveShape.ERODED, TEST_SEED);
terrain, StructureCarveShape.ERODED, TEST_SEED);
}
/**
@@ -2648,8 +2734,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
return true;
}, SLAB_PADDING, 1_000_000);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setShape(IrisStructureCarveShape.ERODED)
.setMode(StructureTerrainMode.FORCE_CARVE)
.setShape(StructureCarveShape.ERODED)
.setHorizontalPadding(SLAB_PADDING)
.setCeilingPadding(8)
.setFloorPadding(0)
@@ -2664,7 +2750,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area,
NativeStructureTerrainIntegrator.organicCarve(
footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED));
footprint, terrain, StructureCarveShape.ERODED, TEST_SEED));
int[] depths = new int[SLAB_DEPTH];
for (int z = 0; z < SLAB_DEPTH; z++) {
@@ -1,5 +1,11 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureSurfaceFitter;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVegetationClearer;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -233,8 +239,8 @@ public class NativeStructurePostProcessorVegetationTest {
}
@Test
public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) {
public void loweringSurfaceKeepsTreeBoundaries() {
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) {
ProtoChunk chunk = chunk();
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
@@ -251,6 +257,34 @@ public class NativeStructurePostProcessorVegetationTest {
}
}
@Test
public void loweringSurfaceRemovesUnsupportedPlankDecoration() {
ProtoChunk chunk = chunk();
for (int x : List.of(4, 5)) {
for (int y = 60; y <= 64; y++) {
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
}
write(chunk, x, 65, 4, Blocks.OAK_PLANKS.defaultBlockState());
write(chunk, x, 66, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, x, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
}
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 60, 4)));
for (int y = 61; y <= 67; y++) {
assertTrue(chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
}
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
assertEquals(Blocks.OAK_PLANKS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 67, 4)));
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
@@ -1,5 +1,9 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTemplateOccupancy;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureReflection;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -1,5 +1,7 @@
package art.arcane.iris.nativegen;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainHeightCache;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.Heightmap;
@@ -1,10 +1,14 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.world.history.TerrainNativeBlockKeys;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTransitionColumn;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import net.minecraft.SharedConstants;
@@ -44,8 +48,8 @@ public final class NativeTransitionColumnTest {
@Test
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
String key = "itemsadder:rocks/ruby_ore";
PlatformBlockState custom = mock(PlatformBlockState.class);
PlatformBlockState carrier = mock(PlatformBlockState.class);
NativeBlockState custom = mock(NativeBlockState.class);
NativeBlockState carrier = mock(NativeBlockState.class);
when(custom.placementBaseState()).thenReturn(carrier);
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
IrisPlatform platform = mock(IrisPlatform.class);
@@ -58,17 +62,17 @@ public final class NativeTransitionColumnTest {
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
platforms.when(IrisPlatforms::isBound).thenReturn(true);
platforms.when(IrisPlatforms::get).thenReturn(platform);
NoiseColumn column = NativeTransitionColumn.column(signature, height);
NoiseColumn column = NativeTransitionColumn.column(signature.geometry(), height, TerrainNativeBlockKeys::placementKey);
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
assertEquals(-14, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.OCEAN_FLOOR_WG, height, TerrainNativeBlockKeys::placementKey));
}
}
@Test
public void preservesNativePropertiesCavesAndFluids() {
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
LevelHeightAccessor.create(-16, 32));
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]").geometry(),
LevelHeightAccessor.create(-16, 32), TerrainNativeBlockKeys::placementKey);
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
@@ -79,14 +83,13 @@ public final class NativeTransitionColumnTest {
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
assertEquals(-12, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.WORLD_SURFACE_WG, height, TerrainNativeBlockKeys::placementKey));
assertEquals(-14, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.OCEAN_FLOOR_WG, height, TerrainNativeBlockKeys::placementKey));
}
@Test
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(signature("missing:removed_block").geometry(), LevelHeightAccessor.create(-16, 32), TerrainNativeBlockKeys::placementKey));
}
private static TerrainBoundarySignature signature(String solidState) {
@@ -1,5 +1,7 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import net.minecraft.SharedConstants;
@@ -28,10 +30,10 @@ public class CustomBiomeSourceStructureContractTest {
IrisDimension upper = new IrisDimension();
upper.setLoadKey("Layers/Upper");
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
assertEquals("layers:upper/aurora", BukkitBiomePolicy.customBiomeKey(upper, "Aurora"));
assertNotEquals(
CustomBiomeSource.customBiomeKey(host, "Aurora"),
CustomBiomeSource.customBiomeKey(upper, "Aurora")
BukkitBiomePolicy.customBiomeKey(host, "Aurora"),
BukkitBiomePolicy.customBiomeKey(upper, "Aurora")
);
}
@@ -39,8 +41,8 @@ public class CustomBiomeSourceStructureContractTest {
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
assertFalse(BukkitBiomePolicy.isBiomeCacheable(temporary, 12, -8));
assertTrue(BukkitBiomePolicy.isBiomeCacheable(stable, 12, -8));
}
private static Engine engineAnsweringNaturalFallback(
@@ -1,6 +1,6 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
@@ -1,5 +1,10 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.nms.BukkitGeneratorContext;
import org.bukkit.World;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeBiomeSourceImpl;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.GenerationSessionManager;
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
@@ -89,8 +94,8 @@ public class IrisChunkGeneratorSpawnAdmissionTest {
}
}
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
private static void setField(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
Field field = NativeChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
@@ -98,7 +103,7 @@ public class IrisChunkGeneratorSpawnAdmissionTest {
private static final class Fixture {
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
private final NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
private final StructureManager structures = mock(StructureManager.class);
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
@@ -110,7 +115,7 @@ public class IrisChunkGeneratorSpawnAdmissionTest {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
ChunkGenerator delegate = mock(ChunkGenerator.class);
CustomBiomeSource source = mock(CustomBiomeSource.class);
NativeBiomeSourceImpl source = mock(NativeBiomeSourceImpl.class);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
@@ -126,7 +131,7 @@ public class IrisChunkGeneratorSpawnAdmissionTest {
});
when(delegate.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
.thenReturn(expected);
setField(generator, "engine", engine);
setField(generator, "context", new BukkitGeneratorContext(new BukkitGeneratorContext.Options(1L, engine, mock(World.class))));
setField(generator, "delegate", delegate);
setField(generator, "customBiomeSource", source);
}
@@ -1,5 +1,7 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureSetFilter;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
@@ -59,9 +61,9 @@ public class NMSBindingDatapackStructureScopeTest {
List.of(),
List.of("managed:illager_barracks"));
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection vanilla = NativeStructureSetFilter.filter(
List.of(managedSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection declaring = NativeStructureSetFilter.filter(
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(0, vanilla.structureSets().size());
@@ -80,9 +82,9 @@ public class NMSBindingDatapackStructureScopeTest {
List.of("managed:tavern"),
List.of());
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection vanilla = NativeStructureSetFilter.filter(
List.of(vanillaSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection declaring = NativeStructureSetFilter.filter(
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(1, vanilla.structureSets().size());
@@ -100,7 +102,7 @@ public class NMSBindingDatapackStructureScopeTest {
"minecraft:custom", structureHolder("managed:only"));
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(unmanagedSet), index, Set.of());
assertEquals(0, selection.structureSets().size());
@@ -124,18 +126,18 @@ public class NMSBindingDatapackStructureScopeTest {
34, 8, RandomSpreadType.LINEAR, 14357620)));
DatapackStructureScopeIndex scopeIndex = index(
List.of(), List.of("managed:illager_barracks"));
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
DatapackStructureStateFilter.keyIndex(List.of(registered));
NativeStructureSetFilter.StructureSetKeyIndex keyIndex =
NativeStructureSetFilter.keyIndex(List.of(registered));
DatapackStructureStateFilter.Selection excluded =
DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection excluded =
NativeStructureSetFilter.filter(
List.of(spigotDirect),
scopeIndex,
Set.of(),
new IrisImportedStructureControl(),
keyIndex);
DatapackStructureStateFilter.Selection retained =
DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection retained =
NativeStructureSetFilter.filter(
List.of(spigotDirect),
scopeIndex,
scopeIndex.declaredSources(List.of(SOURCE)),
@@ -150,8 +152,8 @@ public class NMSBindingDatapackStructureScopeTest {
@Test
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
.getResourceAsStream("DatapackStructureStateFilter.class");
InputStream classResource = NativeStructureSetFilter.class
.getResourceAsStream("NativeStructureSetFilter.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
@@ -176,7 +178,7 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
RandomSpreadStructurePlacement scaledComplexes =
@@ -206,7 +208,7 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
assertSame(custom, selection.structureSets().getFirst());
@@ -238,14 +240,14 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
assertNotSame(dependent, scaledDependent);
assertNotSame(target, scaledTarget);
assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone(
assertSame(scaledTarget, NativeStructureSetFilter.exclusionZone(
scaledDependent.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledTarget.value().placement()).spacing());
@@ -284,14 +286,14 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone(
assertSame(scaledSecond, NativeStructureSetFilter.exclusionZone(
scaledFirst.value().placement()).orElseThrow().otherSet());
assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone(
assertSame(scaledFirst, NativeStructureSetFilter.exclusionZone(
scaledSecond.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledSecond.value().placement()).spacing());
@@ -310,7 +312,7 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
@@ -340,12 +342,12 @@ public class NMSBindingDatapackStructureScopeTest {
List.of("managed:blocked"),
List.of("managed:blocked"));
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(vanillaSet, managedSet), index, Set.of());
assertEquals(1, selection.structureSets().size());
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
assertEquals(0, NativeStructureSetFilter.exclusionZone(scopedPlacement).stream().count());
}
private static DatapackStructureScopeIndex index(
@@ -1,5 +1,7 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructurePoiUpdates;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
@@ -1,9 +1,11 @@
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.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
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;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -84,7 +86,7 @@ public class NativeStructureWorldgenAccessTest {
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
new IrisStructureTerrain().setMode(StructureTerrainMode.FLATTEN)));
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
RecordingDelegate unbounded = new RecordingDelegate();
@@ -1,5 +1,6 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
@@ -83,7 +84,7 @@ public class NmsChunkIOFlushTest {
}
private static final class Fixture {
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
private final NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
private final CraftWorld world = mock(CraftWorld.class);
private final ServerLevel level = mock(ServerLevel.class);
private final RegionDataController chunks = mock(RegionDataController.class);
@@ -0,0 +1,84 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsChunkRetentionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
}
@Test
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
ChunkHolderManager manager = mock(ChunkHolderManager.class);
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
managerField.setAccessible(true);
managerField.set(scheduler, manager);
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
assertTrue(binding.retainChunk(world, 3, -4));
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
assertTrue(type.getValue().doesLoad());
assertFalse(type.getValue().doesSimulate());
assertFalse(type.getValue().persist());
assertTrue(binding.releaseChunk(world, 3, -4));
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
binding.retainChunk(world, 3, -4);
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertSame(type.getValue(), again.getValue());
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
@@ -41,7 +42,7 @@ public class NmsChunkTaskPollingTest {
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CompletableFuture<Void> completed = new CompletableFuture<>();
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
@@ -66,7 +67,7 @@ public class NmsChunkTaskPollingTest {
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
@@ -83,7 +84,7 @@ public class NmsChunkTaskPollingTest {
public void foliaNeverUsesTheWorldWideTaskPump() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
@@ -1,5 +1,6 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
@@ -41,7 +42,7 @@ public class NmsEntitySpawnDifficultyTest {
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
int rejected = 0;
@@ -64,7 +65,7 @@ public class NmsEntitySpawnDifficultyTest {
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
@@ -77,7 +78,7 @@ public class NmsEntitySpawnDifficultyTest {
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
CraftWorld world = mock(CraftWorld.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
@@ -92,7 +93,7 @@ public class NmsEntitySpawnDifficultyTest {
public void unrelatedBukkitSpawnFailuresStillPropagate() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
@@ -1,114 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.history.SavedTerrainChunk;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
public class NmsSavedTerrainCheckpointTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{1, 2, 3};
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
Set<ChunkPos> completed = new HashSet<>();
AtomicBoolean flushed = new AtomicBoolean();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(saved.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(unloaded.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
flushed.set(true);
return null;
});
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
.thenAnswer(invocation -> {
assertTrue(flushed.get());
return null;
});
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
terrain.verifyNoMoreInteractions();
}
}
@Test
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
IOException failure = new IOException("storage flush failed");
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
terrain.verifyNoInteractions();
}
}
@Test
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{4, 5};
IOException failure = new IOException("saved terrain receipt mismatch");
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenReturn(Priority.COMPLETING);
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
}
}
}
@@ -1,223 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
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 NmsSavedTerrainSnapshotTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
Thread owner = Thread.currentThread();
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
PlatformHooks platform = mock(PlatformHooks.class);
CompoundTag capturedData = new CompoundTag();
CompoundTag autosave = new CompoundTag();
List<String> submissions = new ArrayList<>();
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return snapshot;
});
hooks.when(PlatformHooks::get).thenReturn(platform);
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return null;
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
when(snapshot.write()).thenAnswer(invocation -> {
assertNotSame(owner, Thread.currentThread());
assertEquals(List.of("checkpoint", "autosave"), submissions);
return capturedData;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
.thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
submissions.add("checkpoint");
return null;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
submissions.add("autosave");
return null;
});
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
assertEquals(List.of("checkpoint"), submissions);
assertFalse(captured.data().isDone());
verify(snapshot, never()).write();
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
verify(snapshot).write();
}
}
@Test
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
IllegalStateException failure = new IllegalStateException("serialization failed");
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
when(snapshot.write()).thenThrow(failure);
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
assertTrue(captured.data().isCompletedExceptionally());
}
}
@Test
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
World world = mock(World.class);
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
List<NewChunkHolder> holders = new ArrayList<>(129);
for (int index = 0; index < 129; index++) {
NewChunkHolder holder = mock(NewChunkHolder.class);
setField(holder, "chunkX", index);
holders.add(holder);
}
AtomicInteger requested = new AtomicInteger();
AtomicBoolean writesBlocked = new AtomicBoolean(true);
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
Future<?> result = checkpoint.submit(() -> {
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
.thenAnswer(invocation -> {
requested.incrementAndGet();
Runnable action = invocation.getArgument(3);
return CompletableFuture.runAsync(action, owner);
});
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
firstBatchWaiting.countDown();
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
});
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
return null;
}
});
try {
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
assertEquals(64, requested.get());
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
writesBlocked.set(false);
result.get(5, TimeUnit.SECONDS);
assertEquals(129, requested.get());
} finally {
writesBlocked.set(false);
}
}
}
private static void setField(Object instance, String name, Object value) throws Exception {
Field field = instance.getClass().getField(name);
field.setAccessible(true);
field.set(instance, value);
}
private static final class SaveFixture implements AutoCloseable {
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
private final ServerLevel level = mock(ServerLevel.class);
private SaveFixture() throws Exception {
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
setField(scheduler, "saveExecutor", group.createExecutor());
setField(scheduler, "compressionExecutor", group.createExecutor());
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
}
@Override
public void close() throws Exception {
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
}
@@ -1,135 +0,0 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainWriteBarrierTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
ServerLevel level = level(saves, compression);
ChunkPos chunk = new ChunkPos(4, -9);
CountDownLatch generationEntered = new CountDownLatch(1);
CountDownLatch releaseGeneration = new CountDownLatch(1);
AtomicBoolean pendingWrite = new AtomicBoolean(true);
AtomicInteger serialized = new AtomicInteger();
AtomicInteger compressed = new AtomicInteger();
AtomicReference<Thread> saveThread = new AtomicReference<>();
AtomicReference<Thread> compressionThread = new AtomicReference<>();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
generation.queueTask(() -> {
generationEntered.countDown();
try {
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new AssertionError(interruption);
}
});
pool.adjustThreadCount(1);
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
saves.queueTask(() -> {
saveThread.set(Thread.currentThread());
serialized.incrementAndGet();
compression.queueTask(() -> {
compressionThread.set(Thread.currentThread());
compressed.incrementAndGet();
pendingWrite.set(false);
});
});
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
assertEquals(1L, releaseGeneration.getCount());
assertEquals(1, serialized.get());
assertEquals(1, compressed.get());
assertFalse(pendingWrite.get());
assertSame(Thread.currentThread(), saveThread.get());
assertSame(Thread.currentThread(), compressionThread.get());
} finally {
releaseGeneration.countDown();
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
@Test
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
assertTrue(failure.getMessage().contains("Timed out"));
}
@Test
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
Thread.currentThread().interrupt();
try {
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertTrue(failure.getMessage().contains("Interrupted"));
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
savesField.setAccessible(true);
compressionField.setAccessible(true);
savesField.set(scheduler, saves);
compressionField.set(scheduler, compression);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
return level;
}
}
@@ -1,6 +1,8 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainPipeline;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -68,7 +70,7 @@ public class WorldgenTerrainHeightmapsTest {
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
NativeTerrainPipeline.primeAuthoringHeightmaps(chunk);
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
@@ -18,6 +18,7 @@
package art.arcane.iris.structure;
import art.arcane.volmlib.nativelib.common.structure.StructureReaderFixture;
import art.arcane.iris.structure.authoring.StructureCapability;
import art.arcane.iris.structure.authoring.StructureLoss;
import net.minecraft.SharedConstants;
@@ -33,7 +34,6 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class VillageImporterListPoolElementRuntimeContractTest {
@@ -54,7 +54,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructureTemplatePool.Projection.RIGID
);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(towers));
Map<String, Object> entry = VillageImporter.piecePoolEntry(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
1
@@ -85,7 +85,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
0,
List.of(Map.of("name", "minecraft:bottom")));
assertSame(tower, resolution.physicalElement());
assertEquals(tower.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation());
assertEquals(1, resolution.omittedElements());
assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece"));
@@ -103,7 +103,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement waystone = StructurePoolElement
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(waystone));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"dungeons_and_taverns:waystones",
"qa/pool/dungeons_and_taverns/waystones",
@@ -116,7 +116,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
0,
List.of());
assertSame(waystone, resolution.physicalElement());
assertEquals(waystone.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition());
assertEquals(true, normalization.poolEntry().get("empty"));
@@ -130,7 +130,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement orphan = StructurePoolElement
.legacy("test:orphan_house")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(orphan));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:orphan_pool",
"qa/pool/test/orphan_pool",
@@ -143,7 +143,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
37,
List.of());
assertSame(orphan, resolution.physicalElement());
assertEquals(orphan.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals("test:orphan_house", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
@@ -160,7 +160,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement start = StructurePoolElement
.legacy("test:air_start")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(start));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:start",
"qa/pool/test/start",
@@ -173,7 +173,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
0,
List.of());
assertSame(start, resolution.physicalElement());
assertEquals(start.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition());
assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece"));
assertTrue(normalization.losses().isEmpty());
@@ -184,7 +184,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement inert = StructurePoolElement
.legacy("test:mixed_air_inert")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(inert));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
@@ -197,7 +197,7 @@ public class VillageImporterListPoolElementRuntimeContractTest {
0,
List.of());
assertSame(inert, resolution.physicalElement());
assertEquals(inert.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_all_air_mixed_member_omitted",
@@ -214,8 +214,8 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:mixed_non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(allAirElement));
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(nonAirElement));
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
@@ -239,8 +239,8 @@ public class VillageImporterListPoolElementRuntimeContractTest {
41,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(allAirElement.getClass().getSimpleName(), allAirResolution.physicalElement().typeName());
assertEquals(nonAirElement.getClass().getSimpleName(), nonAirResolution.physicalElement().typeName());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece"));
@@ -257,8 +257,8 @@ public class VillageImporterListPoolElementRuntimeContractTest {
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(allAirElement));
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(nonAirElement));
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
@@ -282,8 +282,8 @@ public class VillageImporterListPoolElementRuntimeContractTest {
29,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(allAirElement.getClass().getSimpleName(), allAirResolution.physicalElement().typeName());
assertEquals(nonAirElement.getClass().getSimpleName(), nonAirResolution.physicalElement().typeName());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece"));
@@ -0,0 +1,604 @@
package art.arcane.iris.structure.nativegen;
import art.arcane.iris.structure.nativegen.IrisStructureVolumePolicy;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource;
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.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.IntConsumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructureVolumeIndexTest {
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
assertEquals(STRUCTURE_KEY, volume.structure());
assertEquals(bounds.minX(), volume.minX());
assertEquals(bounds.minY(), volume.minY());
assertEquals(bounds.minZ(), volume.minZ());
assertEquals(bounds.maxX(), volume.maxX());
assertEquals(bounds.maxY(), volume.maxY());
assertEquals(bounds.maxZ(), volume.maxZ());
}
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
}
@Test
public void coldAndCachedQueriesResolveIdenticalVolumes() {
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
int coldResolutions = resolver.resolutions();
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
assertTrue(coldResolutions > 0);
assertFalse(cold.isEmpty());
assertEquals(cold, cached);
assertEquals(coldResolutions, resolver.resolutions());
}
@Test
public void completedOriginsRemainBoundedAndPendingBuildsAreReleased() throws Exception {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(
(engine, chunkX, chunkZ) -> NativeStructureVolume.NONE);
for (int chunkX = -8_192; chunkX <= 8_192; chunkX++) {
index.originVolumes(null, chunkX, -17);
}
assertEquals(16_384, cache(index, "originCache").size());
assertTrue(cache(index, "originBuilds").isEmpty());
index.resolve(null, -16, -16, -1, -1);
assertEquals(16_384, cache(index, "originCache").size());
assertTrue(cache(index, "originBuilds").isEmpty());
assertTrue(cache(index, "queryBuilds").isEmpty());
}
@Test
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
AtomicInteger runtimeId = new AtomicInteger(1);
Engine engine = runtimeEngine(runtimeId);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
runtimeId.set(2);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.evictRuntime(1);
runtimeId.set(1);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(867, resolver.resolutions());
}
@Test
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
assertEquals(289, scoped.scopes().get());
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(306, resolver.resolutions());
assertEquals(306, scoped.scopes().get());
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(306, scoped.scopes().get());
}
@Test
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
ScopedEngine detached = scopedEngine(true, 1);
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
scopedEngine(true, 2), detached)) {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
index.resolve(scoped.engine(), 0, 0, 15, 15);
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(578, scoped.scopes().get());
}
}
@Test
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
index.evictRuntime(1);
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(578, resolver.resolutions());
assertEquals(578, scoped.scopes().get());
}
@Test
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
Set<IntConsumer> listeners = new CopyOnWriteArraySet<>();
doAnswer(invocation -> {
listeners.add(invocation.getArgument(0));
return null;
}).when(engine).addGenerationRuntimeRetirementListener(any());
doAnswer(invocation -> {
listeners.remove(invocation.getArgument(0));
return null;
}).when(engine).removeGenerationRuntimeRetirementListener(any());
NativeStructureVolumeSource.Context context = new NativeStructureVolumeSource.Context(
null, null, null, null, () -> null, () -> null, () -> null);
NativeStructureVolumeIndex.install(engine,
new NativeStructureVolumeSource<>(context, IrisStructureVolumePolicy.INSTANCE)::volumesAt);
try {
assertEquals(1, listeners.size());
IntConsumer original = listeners.iterator().next();
NativeStructureVolumeIndex.invalidate(engine);
assertEquals(1, listeners.size());
assertFalse(listeners.contains(original));
NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15);
NativeStructureVolumeIndex current = installedIndexes().get(engine);
assertEquals(289, cache(current, "originCache").size());
assertEquals(1, cache(current, "queryCache").size());
for (IntConsumer listener : listeners) {
listener.accept(0);
}
assertTrue(cache(current, "originCache").isEmpty());
assertTrue(cache(current, "queryCache").isEmpty());
} finally {
NativeStructureVolumeIndex.uninstall(engine);
}
assertTrue(listeners.isEmpty());
}
@Test
public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
ExecutorService executor = Executors.newFixedThreadPool(2);
Map<?, ?> origins = cache(index, "originCache");
Map<?, ?> queries = cache(index, "queryCache");
try {
Future<KList<NativeStructureVolume>> builder = executor.submit(() -> index.originVolumes(null, 0, 0));
assertTrue(resolver.awaitFirstEntry());
index.originVolumes(null, 1, 0);
assertEquals(1, origins.size());
Future<?> retirement;
synchronized (queries) {
retirement = executor.submit(() -> index.evictRuntime(0));
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
boolean cleared = false;
while (System.nanoTime() < deadline) {
synchronized (origins) {
cleared = origins.isEmpty();
}
if (cleared) {
break;
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
}
assertTrue("Origin eviction did not complete before the query cache lock", cleared);
resolver.release();
assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty());
synchronized (origins) {
assertTrue("The retired in-flight origin was cached again", origins.isEmpty());
}
}
retirement.get(5, TimeUnit.SECONDS);
index.originVolumes(null, 0, 0);
assertEquals(3, resolver.resolutions());
} finally {
resolver.release();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
}
@Test
public void volumesOutsideTheQueryRectAreExcluded() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
KList<NativeStructureVolume> miss = index.resolve(null,
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
assertEquals(1, hit.size());
assertTrue(miss.isEmpty());
}
@Test
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
int reach = NativeStructureVolumeIndex.originReachChunks();
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
}
@Test
public void overlappingColdQueriesResolveIndependentOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
assertTrue(resolver.awaitSecondEntry(5_000));
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
assertEquals(306, resolver.resolutions());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void overlappingNegativeQueriesPreserveEveryVolumeAndOriginOrder() throws Exception {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
});
NativeStructureVolumeIndex sequential = NativeStructureVolumeIndex.forTesting(volumes);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> negative = executor.submit(
() -> index.resolve(null, -16, 0, -1, 15));
assertTrue(coordination.awaitFirstEntry());
Future<KList<NativeStructureVolume>> positive = executor.submit(
() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(coordination.awaitSecondEntry(5_000));
assertEquals(sequential.resolve(null, 0, 0, 15, 15), positive.get(5, TimeUnit.SECONDS));
coordination.release();
assertEquals(sequential.resolve(null, -16, 0, -1, 15), negative.get(5, TimeUnit.SECONDS));
assertEquals(306, coordination.resolutions());
} finally {
coordination.release();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void sharedOriginWaitAllowsQueuedTerrainWorkToRun() throws Exception {
assertQueuedTerrainCanComplete(false);
}
@Test
public void interruptedOriginWaitPreservesInterruptAndResult() throws Exception {
assertQueuedTerrainCanComplete(true);
}
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
CountDownLatch ownerEntered = new CountDownLatch(1);
CompletableFuture<Void> terrain = new CompletableFuture<>();
AtomicBoolean firstOrigin = new AtomicBoolean(true);
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
if (firstOrigin.compareAndSet(true, false)) {
ownerEntered.countDown();
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
try {
Future<KList<NativeStructureVolume>> original = owner.submit(
() -> index.originVolumes(null, -6, -8));
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
firstWaiter.set(Thread.currentThread());
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
return result;
});
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
secondWaiter.set(Thread.currentThread());
return index.resolve(null, 32, 0, 47, 15);
});
awaitOriginWaiter(firstWaiter);
awaitOriginWaiter(secondWaiter);
if (interruptWaiter) {
firstWaiter.get().interrupt();
}
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
terrainTask.get(3, TimeUnit.SECONDS);
assertEquals(1, original.get(3, TimeUnit.SECONDS).size());
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
assertEquals(289, adjacentVolumes.size());
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
assertEquals(interruptWaiter, interruptedAfterQuery.get());
} finally {
terrain.complete(null);
owner.shutdownNow();
workers.shutdownNow();
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
private static void awaitOriginWaiter(AtomicReference<Thread> reference) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
Thread thread = reference.get();
if (thread != null && thread.getState() == Thread.State.WAITING) {
for (StackTraceElement frame : thread.getStackTrace()) {
if (frame.getClassName().equals("art.arcane.volmlib.nativelib.terrain.NativeBuildFutures")
&& frame.getMethodName().equals("awaitBuild")) {
return;
}
}
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
throw new AssertionError("Native query did not wait for the shared origin");
}
@Test
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
int distantChunkX = 64;
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> distant = executor.submit(
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
assertTrue(resolver.awaitSecondEntry(5_000));
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
assertEquals(289, volumes.size());
assertEquals("-8:-8", volumes.getFirst().structure());
assertEquals("0:0", volumes.get(144).structure());
assertEquals("8:8", volumes.getLast().structure());
}
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(engine.getCacheID()).thenReturn(1);
if (ownerRuntimeId != 0) {
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
}
AtomicInteger scopes = new AtomicInteger();
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
scopes.incrementAndGet();
return null;
});
return new ScopedEngine(engine, scopes);
}
@SuppressWarnings("unchecked")
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
indexes.setAccessible(true);
return (Map<Engine, NativeStructureVolumeIndex>) indexes.get(null);
}
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
field.setAccessible(true);
return (Map<?, ?>) field.get(index);
}
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
}
private static Engine runtimeEngine(AtomicInteger runtimeId) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (method.getName().equals("getCacheID")) {
return runtimeId.get();
}
if (method.getName().equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (method.getName().equals("equals")) {
return proxy == arguments[0];
}
if (method.getName().equals("toString")) {
return "runtime-engine-" + runtimeId.get();
}
throw new UnsupportedOperationException(method.getName());
}
);
}
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final int originChunkX;
private final int originChunkZ;
private CountingResolver(int originChunkX, int originChunkZ) {
this.originChunkX = originChunkX;
this.originChunkZ = originChunkZ;
}
private int resolutions() {
return resolutions.get();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
resolutions.incrementAndGet();
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
}
}
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final CountDownLatch firstEntry = new CountDownLatch(1);
private final CountDownLatch secondEntry = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private int resolutions() {
return resolutions.get();
}
private boolean awaitFirstEntry() throws InterruptedException {
return firstEntry.await(5, TimeUnit.SECONDS);
}
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
}
private void release() {
release.countDown();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
int resolution = resolutions.incrementAndGet();
if (resolution == 1) {
firstEntry.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting to release native structure resolution");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
}
} else if (resolution == 2) {
secondEntry.countDown();
}
return NativeStructureVolume.NONE;
}
}
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
}
}
@@ -0,0 +1,12 @@
package art.arcane.volmlib.nativelib.common.structure;
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
public final class StructureReaderFixture {
private StructureReaderFixture() {
}
public static NativeStructureReader.Element element(Object handle) {
return new ReflectiveStructureReader.ElementView(handle, null, ReflectiveStructureReader::readDefaultConnectors);
}
}
@@ -0,0 +1,111 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeRegistryImpl;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeSourceImpl;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
final class HeadlessChunkGenerator extends ChunkGenerator {
private final NativeTerrainColumns columns;
private final int minimumY;
private final int height;
private final int seaLevel;
HeadlessChunkGenerator(Options options) {
super(biomes(options));
Engine engine = options.engine();
columns = new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null));
minimumY = engine.getMinHeight();
height = engine.getHeight();
seaLevel = minimumY + engine.getDimension().getFluidHeight();
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(columns::evictRuntime);
}
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(this);
}
@Override
public int getGenDepth() {
return height;
}
@Override
public int getSeaLevel() {
return seaLevel;
}
@Override
public int getMinY() {
return minimumY;
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor accessor, RandomState random) {
return columns.baseHeight(x, z, type, accessor);
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor accessor, RandomState random) {
return columns.baseColumn(x, z, accessor);
}
@Override
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender, RandomState random,
StructureManager structures, BiomeManager biomes, WorldGenRegion region, Set<Holder<Biome>> possibleBiomes) {
throw new UnsupportedOperationException("Headless terrain must be generated through the Iris region pipeline");
}
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
throw new UnsupportedOperationException("Headless native population is not installed");
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState random, BlockPos position, SamplerContext context) {
result.add("Iris native structure terrain");
}
private static NativeBiomeSourceImpl biomes(Options options) {
NativeBiomeRegistryImpl registry = new NativeBiomeRegistryImpl(options.registries().lookupOrThrow(Registries.BIOME));
BukkitBiomePolicy<Holder<Biome>, Biome> policy = new BukkitBiomePolicy<>(
new BukkitBiomePolicy.RuntimeOptions<>(options.engine().getSeedManager().getSeed(), options.engine(),
null, () -> registry), registry);
return new NativeBiomeSourceImpl(policy);
}
record Options(Engine engine, RegistryAccess registries) {
Options {
Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(registries, "registries");
}
}
}
@@ -0,0 +1,259 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.DataLayer;
import net.minecraft.world.level.chunk.LightChunk;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.lighting.BlockLightEngine;
import net.minecraft.world.level.lighting.ChunkSkyLightSources;
import net.minecraft.world.level.lighting.LayerLightEventListener;
import net.minecraft.world.level.lighting.SkyLightEngine;
import net.minecraft.world.level.material.FluidState;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
final class HeadlessChunkLighting {
private static final int MAXIMUM_WINDOW_CHUNKS = 256;
private static final int HALO = 2;
private HeadlessChunkLighting() {
}
static void light(Request request) {
if (Thread.currentThread().isInterrupted()) {
throw new IllegalStateException("Offline lighting interrupted before initialization");
}
Window window = new Window(request);
BlockLightEngine block = new BlockLightEngine(window);
SkyLightEngine sky = request.skyLight() ? new SkyLightEngine(window) : null;
for (LightingChunk chunk : window.chunks.values()) {
ProtoChunk source = chunk.source;
for (int section = source.getMinSectionY(); section <= source.getMaxSectionY(); section++) {
SectionPos position = SectionPos.of(source.getPos(), section);
boolean empty = source.getSection(source.getSectionIndexFromSectionY(section)).hasOnlyAir();
block.updateSectionStatus(position, empty);
if (sky != null) {
sky.updateSectionStatus(position, empty);
}
}
}
for (LightingChunk chunk : window.chunks.values()) {
block.propagateLightSources(chunk.source.getPos());
if (sky != null) {
sky.propagateLightSources(chunk.source.getPos());
}
}
drain(block);
if (sky != null) {
drain(sky);
}
for (ChunkPos position : request.targets()) {
ProtoChunk chunk = window.chunks.get(position.pack()).source;
StarlightChunk destination = (StarlightChunk) chunk;
destination.starlight$setBlockNibbles(copyLight(block, chunk));
destination.starlight$setSkyNibbles(copyLight(sky, chunk));
boolean[] emptySections = new boolean[chunk.getSectionsCount()];
for (int index = 0; index < emptySections.length; index++) {
emptySections[index] = chunk.getSection(index).hasOnlyAir();
}
destination.starlight$setBlockEmptinessMap(emptySections.clone());
destination.starlight$setSkyEmptinessMap(emptySections);
chunk.setPersistedStatus(ChunkStatus.LIGHT);
chunk.setLightCorrect(true);
}
}
private static void drain(LayerLightEventListener engine) {
while (engine.hasLightWork()) {
if (Thread.currentThread().isInterrupted()) {
throw new IllegalStateException("Offline lighting interrupted before completion");
}
engine.runLightUpdates();
}
}
private static SWMRNibbleArray[] copyLight(LayerLightEventListener engine, ProtoChunk chunk) {
SWMRNibbleArray[] result = new SWMRNibbleArray[chunk.getSectionsCount() + 2];
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int index = 0; index < result.length; index++) {
int sectionY = chunk.getMinSectionY() + index - 1;
DataLayer layer = engine == null ? new DataLayer() : engine.getDataLayerData(SectionPos.of(chunk.getPos(), sectionY));
if (layer == null) {
layer = new DataLayer();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
position.set(chunk.getPos().getMinBlockX() + x, (sectionY << 4) + y,
chunk.getPos().getMinBlockZ() + z);
layer.set(x, y, z, engine.getLightValue(position));
}
}
}
}
result[index] = SWMRNibbleArray.fromVanilla(layer);
}
return result;
}
record Request(List<ProtoChunk> chunks, Set<ChunkPos> targets, boolean skyLight) {
Request {
chunks = List.copyOf(chunks);
targets = Set.copyOf(targets);
if (chunks.isEmpty() || chunks.size() > MAXIMUM_WINDOW_CHUNKS || targets.isEmpty()) {
throw new IllegalArgumentException("Offline lighting requires targets inside a bounded chunk window");
}
}
}
private static final class Window implements LightChunkGetter, BlockGetter {
private final Map<Long, LightingChunk> chunks;
private final int minimumY;
private final int height;
private Window(Request request) {
minimumY = request.chunks().getFirst().getMinY();
height = request.chunks().getFirst().getHeight();
chunks = new HashMap<>(request.chunks().size());
for (ProtoChunk chunk : request.chunks()) {
if (chunk.getMinY() != minimumY || chunk.getHeight() != height
|| chunk.getPersistedStatus() != ChunkStatus.FEATURES) {
throw new IllegalArgumentException("Offline lighting requires matching heights and completed FEATURES chunks");
}
if (chunks.putIfAbsent(chunk.getPos().pack(), new LightingChunk(chunk)) != null) {
throw new IllegalArgumentException("Duplicate lighting chunk: " + chunk.getPos());
}
}
for (ChunkPos target : request.targets()) {
for (int dz = -HALO; dz <= HALO; dz++) {
for (int dx = -HALO; dx <= HALO; dx++) {
if (!chunks.containsKey(ChunkPos.pack(Math.addExact(target.x(), dx), Math.addExact(target.z(), dz)))) {
throw new IllegalArgumentException("Offline lighting target lacks its completed neighbor halo: " + target);
}
}
}
}
}
@Override
public LightChunk getChunkForLighting(int x, int z) {
return chunks.get(ChunkPos.pack(x, z));
}
@Override
public BlockGetter getLevel() {
return this;
}
@Override
public int getMinY() {
return minimumY;
}
@Override
public int getHeight() {
return height;
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return requireChunk(position).getBlockEntity(position);
}
@Override
public BlockState getBlockState(BlockPos position) {
return requireChunk(position).getBlockState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
LightingChunk chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
return chunk == null ? null : chunk.getBlockState(position);
}
@Override
public FluidState getFluidState(BlockPos position) {
return getBlockState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
BlockState state = getBlockStateIfLoaded(position);
return state == null ? null : state.getFluidState();
}
private LightingChunk requireChunk(BlockPos position) {
return Objects.requireNonNull(chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4)),
"Lighting query outside the supplied chunk window");
}
}
private static final class LightingChunk implements LightChunk {
private final ProtoChunk source;
private final ChunkSkyLightSources skySources;
private LightingChunk(ProtoChunk source) {
this.source = source;
skySources = new ChunkSkyLightSources(source);
skySources.fillFrom(source);
}
@Override
public void findBlockLightSources(BiConsumer<BlockPos, BlockState> consumer) {
source.findBlockLightSources(consumer);
}
@Override
public ChunkSkyLightSources getSkyLightSources() {
return skySources;
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return source.getBlockEntity(position);
}
@Override
public BlockState getBlockState(BlockPos position) {
return source.getBlockState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return source.getBlockStateIfLoaded(position);
}
@Override
public FluidState getFluidState(BlockPos position) {
return source.getFluidState(position);
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
return source.getFluidIfLoaded(position);
}
@Override
public int getMinY() {
return source.getMinY();
}
@Override
public int getHeight() {
return source.getHeight();
}
}
}
@@ -0,0 +1,126 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
import ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.chunk.DataLayer;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkType;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class HeadlessChunkSerialization {
private HeadlessChunkSerialization() {
}
static SerializableChunkData copyOf(ProtoChunk chunk, Context context) {
if (!chunk.canBeSerialized() || chunk.getPersistedStatus().getChunkType() != ChunkType.PROTOCHUNK) {
throw new IllegalArgumentException("Headless serialization requires a serializable proto chunk");
}
List<CompoundTag> blockEntities = new ArrayList<>(chunk.getBlockEntitiesPos().size());
for (BlockPos position : chunk.getBlockEntitiesPos()) {
CompoundTag tag = chunk.getBlockEntityNbtForSaving(position, context.structures().registryAccess());
if (tag != null) {
blockEntities.add(tag.copy());
}
}
List<CompoundTag> entities = new ArrayList<>(chunk.getEntities().size());
for (CompoundTag entity : chunk.getEntities()) {
entities.add(entity.copy());
}
Map<Heightmap.Types, long[]> heightmaps = new EnumMap<>(Heightmap.Types.class);
for (Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
heightmaps.put(entry.getKey(), entry.getValue().getRawData().clone());
}
ShortList[] postProcessing = new ShortList[chunk.getPostProcessing().length];
for (int i = 0; i < postProcessing.length; i++) {
ShortList source = chunk.getPostProcessing()[i];
if (source != null && !source.isEmpty()) {
postProcessing[i] = new ShortArrayList(source);
}
}
CompoundTag persistent = chunk.persistentDataContainer.isEmpty()
? null : chunk.persistentDataContainer.toTagCompound().copy();
return new SerializableChunkData(context.containers(), chunk.getPos(), chunk.getMinSectionY(),
context.gameTime(), chunk.getInhabitedTime(), chunk.getPersistedStatus(),
chunk.getBlendingData() == null ? null : chunk.getBlendingData().pack(),
chunk.getBelowZeroRetrogen(), chunk.getUpgradeData().copy(), heightmaps,
chunk.getTicksForSerialization(context.gameTime()), postProcessing, chunk.isLightCorrect(),
sections(chunk), entities, blockEntities, structures(chunk, context.structures()), persistent);
}
private static List<SerializableChunkData.SectionData> sections(ProtoChunk chunk) {
LevelChunkSection[] blocks = chunk.getSections();
StarlightChunk light = (StarlightChunk) chunk;
SWMRNibbleArray[] blockLight = light.starlight$getBlockNibbles();
SWMRNibbleArray[] skyLight = light.starlight$getSkyNibbles();
List<SerializableChunkData.SectionData> sections = new ArrayList<>(blocks.length + 2);
for (int i = 0; i < blocks.length + 2; i++) {
LevelChunkSection blocksCopy = i > 0 && i <= blocks.length ? blocks[i - 1].copy() : null;
SWMRNibbleArray.SaveState blockState = blockLight[i].getSaveState();
SWMRNibbleArray.SaveState skyState = skyLight[i].getSaveState();
if (blocksCopy == null && blockState == null && skyState == null) {
continue;
}
SerializableChunkData.SectionData section = new SerializableChunkData.SectionData(
chunk.getMinSectionY() + i - 1, blocksCopy, layer(blockState), layer(skyState));
if (blockState != null) {
((StarlightSectionData) (Object) section).starlight$setBlockLightState(blockState.state);
}
if (skyState != null) {
((StarlightSectionData) (Object) section).starlight$setSkyLightState(skyState.state);
}
sections.add(section);
}
return sections;
}
private static DataLayer layer(SWMRNibbleArray.SaveState state) {
return state == null || state.data == null ? null : new DataLayer(state.data);
}
private static CompoundTag structures(ProtoChunk chunk, StructurePieceSerializationContext context) {
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
CompoundTag starts = new CompoundTag();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
starts.put(Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString(),
entry.getValue().createTag(context, chunk.getPos()));
}
CompoundTag references = new CompoundTag();
for (Map.Entry<Structure, LongSet> entry : chunk.getAllReferences().entrySet()) {
if (!entry.getValue().isEmpty()) {
references.putLongArray(Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString(),
entry.getValue().toLongArray());
}
}
CompoundTag result = new CompoundTag();
result.put("starts", starts);
result.put("References", references);
return result;
}
record Context(StructurePieceSerializationContext structures, PalettedContainerFactory containers, long gameTime) {
Context {
Objects.requireNonNull(structures, "structures");
Objects.requireNonNull(containers, "containers");
}
}
}
@@ -0,0 +1,33 @@
package art.arcane.iris.probe;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import java.util.List;
import java.util.function.Supplier;
record HeadlessNativeBiomeWriter(Supplier<HolderLookup.Provider> registries) implements PlatformBiomeWriter {
@Override
public int biomeIdFor(String key) {
Registry<Biome> registry = registry();
Biome biome = registry.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(key))).value();
return registry.getId(biome);
}
@Override
public List<NativeBiome> allBiomes() {
return registry().listElements().map(holder -> (NativeBiome) ModdedBiome.of(
holder.value(), holder.key().identifier().toString())).toList();
}
private Registry<Biome> registry() {
return (Registry<Biome>) registries.get().lookupOrThrow(Registries.BIOME);
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.probe;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.component.DataComponentInitializers;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.tags.TagLoader;
import java.util.List;
final class HeadlessNativeBootstrap {
private static boolean initialized;
private HeadlessNativeBootstrap() {
}
static synchronized void bindComponents(HolderLookup.Provider registries) {
List<DataComponentInitializers.PendingComponents<?>> pending =
BuiltInRegistries.DATA_COMPONENT_INITIALIZERS.build(registries);
for (DataComponentInitializers.PendingComponents<?> components : pending) {
components.apply();
}
}
static synchronized void initialize() {
if (initialized) {
return;
}
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
List.of(ServerPacksSource.createVanillaPackSource().fullResources()))) {
List<Registry.PendingTags<?>> pending = TagLoader.loadTagsForExistingRegistries(resources,
RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY));
for (Registry.PendingTags<?> tags : pending) {
tags.apply();
}
}
initialized = true;
}
}
@@ -0,0 +1,273 @@
package art.arcane.iris.probe;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
import art.arcane.volmlib.nativelib.item.NativeItem;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeGenerationRegistryImpl;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries, NativeBlockResolver.Policy {
private final StubPlatform services;
private final NativeRegistryAccess nativeAccess;
private final PlatformBiomeWriter biomeWriter;
private final PlatformGenerationRegistry generationRegistry;
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
private final NativeBlockState air = blocks.getAir();
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
HeadlessNativePlatform(File root, Supplier<HolderLookup.Provider> registries) {
services = new StubPlatform(root);
biomeWriter = new HeadlessNativeBiomeWriter(registries);
generationRegistry = new BukkitGenerationRegistry(new NativeGenerationRegistryImpl(() -> {
HolderLookup.Provider lookup = registries.get();
if (lookup instanceof RegistryAccess access) {
return access;
}
throw new IllegalStateException("Generation registry contract requires loaded native registries");
}), "bukkit-generation-registry-v1");
nativeAccess = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(registries,
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
message -> { throw new IllegalStateException("Unavailable native registry: " + message); }));
}
NativeBlockResolver blocks() {
return blocks;
}
@Override
public PlatformRegistries registries() {
return this;
}
@Override
public PlatformGenerationRegistry generationRegistry() {
return generationRegistry;
}
@Override
public String platformName() {
return "headless-native-core";
}
@Override
public String minecraftVersion() {
return SharedConstants.getCurrentVersion().name();
}
@Override
public NativeBlockState decodeBlockState(String key) {
return states.computeIfAbsent(key, NativeBlockResolver::strictParse);
}
@Override
public NativeBlockState block(String key) {
return states.computeIfAbsent(key, candidate -> Objects.requireNonNull(blocks.getOrNull(candidate, true),
"Unresolved native block: " + candidate));
}
@Override
public NativeBlockState blockOrNull(String key) {
return blocks.getOrNull(key);
}
@Override
public NativeBlockState blockOrNull(String key, boolean warn) {
return blocks.getOrNull(key, warn);
}
@Override
public NativeBlockState air() {
return air;
}
@Override
public ModdedBlockState resolveCustomBlock(String key) {
return null;
}
@Override
public boolean preventLeafDecay() {
return IrisSettings.get().getGenerator().isPreventLeafDecay();
}
@Override
public void reportError(String key, Throwable failure) {
services.reportError(failure);
failure.printStackTrace(System.err);
}
@Override
public void warnUnresolved(String key, String message) {
throw new IllegalArgumentException(message + ": " + key);
}
@Override
public void debug(String message) {
services.log(LogLevel.DEBUG, message);
}
@Override
public NativeBlockState deepSlateOre(NativeBlockState block, NativeBlockState ore) {
return nativeAccess.deepSlateOre(block, ore);
}
@Override
public NativeBiome biome(String key) {
return nativeAccess.biome(key);
}
@Override
public NativeItem item(String key) {
return nativeAccess.item(key);
}
@Override
public NativeEntityType entity(String key) {
return nativeAccess.entity(key);
}
@Override
public List<String> blockKeys() {
return nativeAccess.blockKeys();
}
@Override
public List<String> biomeKeys() {
return nativeAccess.biomeKeys();
}
@Override
public List<String> structureKeys() {
return nativeAccess.structureKeys();
}
@Override
public List<String> itemKeys() {
return nativeAccess.itemKeys();
}
@Override
public List<String> entityKeys() {
return nativeAccess.entityKeys();
}
@Override
public List<String> enchantmentKeys() {
return nativeAccess.enchantmentKeys();
}
@Override
public List<String> potionEffectKeys() {
return nativeAccess.potionEffectKeys();
}
@Override
public List<String> lootTableKeys() {
return nativeAccess.lootTableKeys();
}
@Override
public Map<String, List<NativeBlockProperty>> blockStateProperties() {
return nativeAccess.blockStateProperties();
}
@Override
public List<String> blockTypeKeys() {
return blockKeys();
}
@Override
public PlatformScheduler scheduler() {
return services.scheduler();
}
@Override
public PlatformStructureHooks structureHooks() {
return services.structureHooks();
}
@Override
public PlatformBiomeWriter biomeWriter() {
return biomeWriter;
}
@Override
public File dataFolder() {
return services.dataFolder();
}
@Override
public File dataFile(String... path) {
return services.dataFile(path);
}
@Override
public File pluginJar() {
return services.pluginJar();
}
@Override
public int irisVersionNumber() {
return services.irisVersionNumber();
}
@Override
public int minecraftVersionNumber() {
return Integer.parseInt(minecraftVersion().replace(".", ""));
}
@Override
public void callEvent(Object event) {
services.callEvent(event);
}
@Override
public void dispatchConsoleCommand(String command) {
services.dispatchConsoleCommand(command);
}
@Override
public boolean spawnEntity(NativeWorld world, String entityKey, double x, double y, double z) {
return services.spawnEntity(world, entityKey, x, y, z);
}
@Override
public void log(LogLevel level, String message) {
services.log(level, message);
}
@Override
public void msg(String message) {
services.msg(message);
}
@Override
public void reportError(Throwable error) {
services.reportError(error);
}
}
@@ -0,0 +1,116 @@
package art.arcane.iris.probe;
import art.arcane.iris.pack.datapack.IrisDatapackCompiler;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.RegistryDataLoader;
import net.minecraft.server.packs.PackLocationInfo;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.PathPackResources;
import net.minecraft.server.packs.repository.PackSource;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import java.io.IOException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
final class HeadlessNativeRegistries implements AutoCloseable {
private final Path directory;
private final MultiPackResourceManager resources;
private final RegistryAccess.Frozen registries;
private HeadlessNativeRegistries(Path directory, MultiPackResourceManager resources, RegistryAccess.Frozen registries) {
this.directory = directory;
this.resources = resources;
this.registries = registries;
}
static HeadlessNativeRegistries load(Map<String, String> definitions) throws IOException {
HeadlessNativeBootstrap.initialize();
Path directory = Files.createTempDirectory("iris-native-registry-");
try {
for (Map.Entry<String, String> definition : definitions.entrySet()) {
Path file = directory.resolve(definition.getKey()).normalize();
if (!file.startsWith(directory.resolve("data")) || !file.toString().endsWith(".json")) {
throw new IllegalArgumentException("Invalid generated registry resource " + definition.getKey());
}
Files.createDirectories(file.getParent());
Files.writeString(file, definition.getValue());
}
return load(directory);
} catch (IOException | RuntimeException failure) {
delete(directory);
throw failure;
}
}
static HeadlessNativeRegistries compile(File pack) throws IOException {
HeadlessNativeBootstrap.initialize();
Path directory = Files.createTempDirectory("iris-native-pack-");
try {
IrisDatapackCompiler.compile(List.of(pack), new KList<>(directory.toFile()), List.of(),
new DataFixerV263(), false);
return load(directory);
} catch (IOException | RuntimeException failure) {
delete(directory);
throw failure;
}
}
private static HeadlessNativeRegistries load(Path directory) {
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
PackSource.WORLD, Optional.empty());
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
ServerPacksSource.createVanillaPackSource().fullResources(), new PathPackResources(location, directory)));
try {
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
RegistryAccess.Frozen loaded;
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
}
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(
Stream.concat(builtins.registries(), loaded.registries())).freeze();
HeadlessNativeBootstrap.bindComponents(registries);
return new HeadlessNativeRegistries(directory, resources, registries);
} catch (RuntimeException failure) {
resources.close();
throw failure;
}
}
RegistryAccess.Frozen registries() {
return registries;
}
MultiPackResourceManager resources() {
return resources;
}
@Override
public void close() throws IOException {
resources.close();
delete(directory);
}
private static void delete(Path directory) throws IOException {
try (Stream<Path> paths = Files.walk(directory)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}
@@ -0,0 +1,109 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.block.BlockDataMergeSupport;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.DecoratorPlatformHooks;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.modded.ModdedDecoratorHooks;
import art.arcane.iris.modded.ModdedStateRotator;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStateMerger;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.server.packs.resources.ResourceManager;
final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBindings {
private final Path stagingRoot;
private NativeRegionTerrainWriter writer;
private HeadlessNativeRegistries nativeRegistries;
private HolderLookup.Provider registries;
HeadlessNativeRuntime() {
stagingRoot = null;
}
HeadlessNativeRuntime(Path stagingRoot) throws IOException {
this.stagingRoot = stagingRoot.toRealPath();
}
@Override
public IrisPlatform create(File root) {
HeadlessNativeBootstrap.initialize();
registries = VanillaRegistries.createWorldLookup();
HeadlessNativePlatform platform = new HeadlessNativePlatform(root, () -> registries);
ModdedDecoratorHooks decorators = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decorators, decorators);
IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindPlatformMerger(new NativeStateMerger(platform.blocks())::merge);
NativeTileReader tiles = new NativeTileReader(() -> registries);
TileData.bindPlatformReader(input -> ModdedTileData.wrap(tiles.read(input)));
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
return platform;
}
@Override
public void prepare(IrisData data, IrisDimension dimension) {
try {
if (nativeRegistries != null) {
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
}
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
registries = nativeRegistries.registries();
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
} catch (IOException failure) {
throw new UncheckedIOException("Cannot load native generation registries", failure);
}
}
@Override
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
if (stagingRoot != null) {
if (!request.worldRoot().getParent().toRealPath().startsWith(stagingRoot)) {
throw new IOException("Unpublished native generation must remain inside its private staging directory");
}
return HeadlessGenerationHistorySession.createUnpublished(request, new DataFixerV263());
}
return HeadlessGenerationHistorySession.create(request, new DataFixerV263());
}
@Override
public void close() throws IOException {
if (nativeRegistries != null) {
nativeRegistries.close();
nativeRegistries = null;
}
}
RegistryAccess.Frozen registries() {
return nativeRegistries.registries();
}
ResourceManager resources() {
return nativeRegistries.resources();
}
NativeRegionTerrainWriter writer() {
return writer;
}
}
@@ -0,0 +1,211 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainPipeline;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.WorldgenTerrainHeightmaps;
import net.minecraft.core.Holder;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
final class HeadlessRegionTerrain {
private static final NamespacedKey STRUCTURE_ACTIVATION = new NamespacedKey("iris", "structure_activation");
private static final NamespacedKey NATURAL_TERRAIN = new NamespacedKey("iris", "natural_terrain");
private HeadlessRegionTerrain() {
}
static final class Session {
private final Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks = new HashMap<>();
private final RegionGenerationWindow workers;
private HeadlessTerrainContext context;
Session(RegionGenerationWindow workers) {
this.workers = Objects.requireNonNull(workers, "workers");
}
Result generate(Request request, Consumer<String> progress) throws Exception {
workers.requireOpen();
if (context != null && context != request.context()) {
throw new IllegalArgumentException("Rolling regions require one native terrain context");
}
context = request.context();
List<NativeRegionPlan.PlannedChunk> plan = NativeRegionPlan.plan(
request.chunkX(), request.chunkZ(), request.width());
Set<Long> retained = new HashSet<>(plan.size());
for (NativeRegionPlan.PlannedChunk planned : plan) {
retained.add(ChunkPos.pack(planned.x(), planned.z()));
}
chunks.keySet().retainAll(retained);
Map<Long, ChunkStatus> previous = new HashMap<>(chunks.size());
for (Map.Entry<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> entry : chunks.entrySet()) {
previous.put(entry.getKey(), entry.getValue().chunk().getPersistedStatus());
}
for (ChunkStatus status : List.of(ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_REFERENCES,
ChunkStatus.BIOMES, ChunkStatus.TERRAIN)) {
List<NativeRegionPlan.PlannedChunk> eligible = new ArrayList<>();
for (NativeRegionPlan.PlannedChunk planned : plan) {
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> existing =
chunks.get(ChunkPos.pack(planned.x(), planned.z()));
if (planned.status().isOrAfter(status)
&& (status != ChunkStatus.BIOMES || planned.status() == ChunkStatus.BIOMES)
&& (existing == null || existing.chunk().getPersistedStatus().isBefore(status))) {
eligible.add(planned);
}
}
if (eligible.isEmpty()) {
continue;
}
long started = System.nanoTime();
AtomicInteger completed = new AtomicInteger();
workers.process(new RegionGenerationWindow.Request<>(eligible.size(), request.parallelism(),
index -> apply(request.context(), eligible.get(index), status, chunks),
(index, result) -> {
if (status == ChunkStatus.STRUCTURE_STARTS) {
chunks.put(result.chunk().getPos().pack(), result);
}
int count = completed.incrementAndGet();
if (count % 128 == 0 || count == eligible.size()) {
progress.accept("stage=" + status.getName() + " chunks=" + count + "/" + eligible.size()
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
}
}));
failReported();
}
List<ProtoChunk> ordered = new ArrayList<>(plan.size());
List<ProtoChunk> updated = new ArrayList<>();
for (NativeRegionPlan.PlannedChunk planned : plan) {
ProtoChunk chunk = chunks.get(ChunkPos.pack(planned.x(), planned.z())).chunk();
if (chunk.getPersistedStatus().isBefore(planned.status())) {
throw new IllegalStateException("Incomplete native stage at " + chunk.getPos());
}
ordered.add(chunk);
ChunkStatus prior = previous.get(chunk.getPos().pack());
if (prior == null || chunk.getPersistedStatus().isAfter(prior)) {
updated.add(chunk);
}
}
return new Result(List.copyOf(ordered), List.copyOf(updated), request.width() * request.width());
}
}
private static HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> apply(
HeadlessTerrainContext context, NativeRegionPlan.PlannedChunk planned, ChunkStatus status,
Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks) throws Exception {
Engine engine = context.engine();
GenerationHistoryRuntimeRouter router = ((IrisEngine) engine).getGenerationHistoryRuntimeRouter().orElseThrow();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(planned.x(), planned.z());
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope();
GenerationSessionLease lease = engine.acquireGenerationLease("headless_native_terrain");
IrisContext.Scope generation = IrisContext.open(engine, lease.sessionId(), null)) {
if (status == ChunkStatus.STRUCTURE_STARTS) {
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> result = HeadlessStructurePlanner.plan(
context.structures(), context.policies(), new ChunkPos(planned.x(), planned.z()));
result.chunk().persistentDataContainer.set(STRUCTURE_ACTIVATION,
PersistentDataType.LONG, route.activation().activationId());
return result;
}
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> result = Objects.requireNonNull(
chunks.get(ChunkPos.pack(planned.x(), planned.z())), "planned structure chunk");
if (status == ChunkStatus.STRUCTURE_REFERENCES) {
HeadlessStructurePlanner.references(context.policies(), result, chunks);
} else if (status == ChunkStatus.BIOMES) {
biomes(context, result.chunk());
} else if (status == ChunkStatus.TERRAIN) {
if (result.chunk().getPersistedStatus().isBefore(ChunkStatus.BIOMES)) {
biomes(context, result.chunk());
}
terrain(context, result.chunk(), route);
} else {
throw new IllegalArgumentException("Unsupported headless native stage " + status);
}
return result;
}
}
private static void biomes(HeadlessTerrainContext context, ProtoChunk chunk) {
context.biomes().prepareVisibleBiomeBatch();
NativeBiomeSourcePolicy.VisibleResolver<Holder<Biome>> resolver = context.biomes().visibleResolver();
chunk.fillBiomesFromNoise(resolver::biome);
chunk.setPersistedStatus(ChunkStatus.BIOMES);
}
private static void terrain(HeadlessTerrainContext context, ProtoChunk target,
GenerationHistoryRuntimeRouter.RuntimeRoute route) throws Exception {
Engine engine = context.engine();
ChunkPos position = target.getPos();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
engine.generate(position.x() << 4, position.z() << 4, blocks, biomes, true);
context.writer().fill(target, new NativeRegionTerrainWriter.TerrainInput(position.x(), position.z(),
engine.getMinHeight(), engine.getHeight(),
(x, y, z) -> blockState(blocks.getRaw(x, y, z)),
(x, y, z) -> Objects.requireNonNull(biomes.getRaw(x, y, z), "Generated native biome").key(), List.of()));
NativeTerrainPipeline.claimGeneratedSemantics(route, target, engine.getMinHeight());
target.persistentDataContainer.set(NATURAL_TERRAIN, PersistentDataType.BYTE_ARRAY,
NativeTerrainReceipt.encode(route.naturalTerrain().orElseThrow(),
route.activation().activationId(), route.epoch().epochId()));
WorldgenTerrainHeightmaps.primeTerrain(target,
(x, z) -> engine.getHeight(x, z, false) + engine.getMinHeight() + 1,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight() + 1);
Heightmap.primeHeightmaps(target, EnumSet.of(Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE));
target.setPersistedStatus(ChunkStatus.TERRAIN);
target.markUnsaved();
}
private static BlockState blockState(NativeBlockState state) {
return state == null ? Blocks.AIR.defaultBlockState() : ((ModdedBlockState) state).handle();
}
private static void failReported() {
List<Throwable> reports = RealPackProbeSupport.drainReported();
if (!reports.isEmpty()) {
IllegalStateException failure = new IllegalStateException("Native terrain generation reported errors");
for (Throwable report : reports) {
failure.addSuppressed(report);
}
throw failure;
}
}
record Request(HeadlessTerrainContext context, int chunkX, int chunkZ, int width, int parallelism) {
Request {
Objects.requireNonNull(context, "context");
if (parallelism < 1 || parallelism > 32) {
throw new IllegalArgumentException("Headless native parallelism must be 1..32");
}
}
}
record Result(List<ProtoChunk> chunks, List<ProtoChunk> updated, int targetChunks) {
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.probe;
import net.kyori.adventure.key.Key;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.spigotmc.SpigotConfig;
import org.spigotmc.SpigotWorldConfig;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
final class HeadlessStructureConfiguration {
private static final int CONFIG_VERSION = 13;
private static final Field CONFIG_FILE = nativeField("CONFIG_FILE");
private static final Field VERSION = nativeField("version");
private HeadlessStructureConfiguration() {
}
static SpigotWorldConfig load(Options options) throws IOException {
if (Bukkit.getServer() != null) {
throw new IllegalStateException("Headless structure configuration cannot initialize inside a Bukkit server");
}
Files.createDirectories(options.stagingDirectory());
Path staged = Files.createTempFile(options.stagingDirectory(), "spigot-structures-", ".yml");
try {
Files.copy(options.source(), staged, StandardCopyOption.REPLACE_EXISTING);
YamlConfiguration configuration = new YamlConfiguration();
configuration.load(staged.toFile());
configuration.options().copyDefaults(true);
int version = configuration.getInt("config-version", CONFIG_VERSION);
String worldName = options.levelKey().identifier().toString();
prepareEnvironment(configuration, options.legacyWorldName(), worldName, version);
configuration.set("config-version", CONFIG_VERSION);
return initialize(options, staged, configuration);
} catch (InvalidConfigurationException | ReflectiveOperationException | RuntimeException failure) {
throw new IOException("Could not initialize native structure configuration from " + options.source(), failure);
} finally {
Files.deleteIfExists(staged);
}
}
private static void prepareEnvironment(YamlConfiguration configuration, String legacyWorldName,
String worldName, int version) {
if (version <= 12) {
ConfigurationSection legacy = configuration.getConfigurationSection("world-settings." + legacyWorldName);
if (legacy != null) {
configuration.set("world-settings." + legacyWorldName, null);
configuration.set("world-settings." + worldName, legacy);
}
}
String worldPath = "world-settings." + worldName + ".";
configuration.set(worldPath + "verbose", false);
configuration.set(worldPath + "view-distance", 3);
configuration.set(worldPath + "simulation-distance", 3);
}
private static SpigotWorldConfig initialize(Options options, Path staged, YamlConfiguration configuration)
throws ReflectiveOperationException {
synchronized (SpigotConfig.class) {
YamlConfiguration previousConfiguration = SpigotConfig.config;
File previousFile = (File) CONFIG_FILE.get(null);
int previousVersion = VERSION.getInt(null);
try {
SpigotConfig.config = configuration;
CONFIG_FILE.set(null, staged.toFile());
VERSION.setInt(null, CONFIG_VERSION);
return new SpigotWorldConfig(options.legacyWorldName(), Key.key(options.levelKey().identifier().toString()));
} finally {
SpigotConfig.config = previousConfiguration;
CONFIG_FILE.set(null, previousFile);
VERSION.setInt(null, previousVersion);
}
}
}
private static Field nativeField(String name) {
try {
Field field = SpigotConfig.class.getDeclaredField(name);
field.setAccessible(true);
return field;
} catch (ReflectiveOperationException failure) {
throw new ExceptionInInitializerError(failure);
}
}
record Options(Path source, String legacyWorldName, ResourceKey<Level> levelKey, Path stagingDirectory) {
Options {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(legacyWorldName, "legacyWorldName");
Objects.requireNonNull(levelKey, "levelKey");
Objects.requireNonNull(stagingDirectory, "stagingDirectory");
}
}
}
@@ -0,0 +1,333 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
import art.arcane.volmlib.nativelib.terrain.structure.StructureInjectionPolicy;
import art.arcane.volmlib.nativelib.terrain.structure.StructureOwnershipRecordView;
import art.arcane.volmlib.nativelib.terrain.structure.StructurePlacementDecision;
import art.arcane.volmlib.nativelib.terrain.structure.StructureReferencePolicy;
import art.arcane.volmlib.nativelib.terrain.structure.StructureStartPlan;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureFactory;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipFingerprint;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceRepair;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVegetationClearer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.WorldOptions;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
final class HeadlessStructurePlanner {
private HeadlessStructurePlanner() {
}
static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> PlannedChunk<O> plan(
NativeContext context, Policies<P, O> policies, ChunkPos position) {
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY,
LevelHeightAccessor.create(context.generator().getMinY(), context.generator().getGenDepth()),
PalettedContainerFactory.create(context.registries()), null);
if (!context.enabled()) {
chunk.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
return new PlannedChunk<O>(context, chunk, Map.of(), Map.of());
}
try (NativeGenerationScope scope = policies.references().openOriginScope(chunk.getPos().x(), chunk.getPos().z())) {
generateNaturalStarts(context, chunk);
Map<Structure, O> ownership = inject(context, policies, chunk);
Map<Structure, StructureTerrainSettings> terrain = adjust(context, policies, chunk, ownership);
chunk.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
return new PlannedChunk<>(context, chunk, Map.copyOf(ownership), Map.copyOf(terrain));
}
}
static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> void references(
Policies<P, O> policies, PlannedChunk<O> target, Map<Long, PlannedChunk<O>> grid) {
ChunkPos position = target.chunk().getPos();
List<PlannedChunk<O>> origins = new ArrayList<>(289);
if (target.chunk().getPersistedStatus() != ChunkStatus.STRUCTURE_STARTS) {
throw new IllegalArgumentException("Structure references require an unreferenced planned chunk: " + position);
}
if (!target.context().adapterEnabled()) {
target.chunk().setPersistedStatus(ChunkStatus.STRUCTURE_REFERENCES);
return;
}
for (int x = position.x() - 8; x <= position.x() + 8; x++) {
for (int z = position.z() - 8; z <= position.z() + 8; z++) {
PlannedChunk<O> origin = grid.get(ChunkPos.pack(x, z));
if (origin == null || origin.context() != target.context()
|| origin.chunk().getPos().x() != x || origin.chunk().getPos().z() != z
|| !origin.chunk().getPersistedStatus().isOrAfter(ChunkStatus.STRUCTURE_STARTS)) {
throw new IllegalArgumentException("Missing matching structure origin at " + x + "," + z);
}
origins.add(origin);
}
}
Map<Structure, LongSet> references = new HashMap<>();
Registry<Structure> registry = target.context().registries().lookupOrThrow(Registries.STRUCTURE);
for (PlannedChunk<O> origin : origins) {
ChunkPos source = origin.chunk().getPos();
try (NativeGenerationScope scope = policies.references().openOriginScope(source.x(), source.z())) {
for (Map.Entry<Structure, StructureStart> entry : origin.chunk().getAllStarts().entrySet()) {
StructureStart start = entry.getValue();
if (!start.isValid()) {
continue;
}
String key = Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString();
if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) {
throw new IllegalStateException("Planned structure exceeds its reference range: " + key);
}
O ownership = origin.ownership().get(entry.getKey());
boolean relevant = ownership == null
? NativeStructureReferenceRepair.requiresNaturalReference(policies.references(), position,
key, entry.getKey(), start)
: NativeStructureReferenceRepair.requiresReference(position, key, start, ownership);
if (relevant) {
references.computeIfAbsent(entry.getKey(), ignored -> new LongOpenHashSet()).add(source.pack());
}
}
}
}
target.chunk().setAllReferences(references);
target.chunk().setPersistedStatus(ChunkStatus.STRUCTURE_REFERENCES);
}
private static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> Map<Structure, O> inject(
NativeContext context, Policies<P, O> policies, ProtoChunk chunk) {
Registry<Structure> registry = context.registries().lookupOrThrow(Registries.STRUCTURE);
Map<Structure, O> ownership = new LinkedHashMap<>();
for (P plan : policies.injection().plansAt(chunk.getPos().x(), chunk.getPos().z())) {
Holder.Reference<Structure> holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE,
Identifier.parse(plan.structureKey())));
Structure structure = holder.value();
if (ownership.containsKey(structure)) {
policies.injection().duplicate(plan.structureKey());
continue;
}
StructureStart existing = chunk.getStartForStructure(structure);
boolean replace = plan.replacesSource() || existing != null && existing.isValid()
&& policies.injection().sourceReplaced(plan.structureKey(),
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
if (!replace && existing != null && existing.isValid()) {
continue;
}
int references = existing != null && existing.isValid() ? existing.getReferences() : 0;
StructureStart generated = NativeStructureFactory.generate(new NativeStructureFactory.GenerationContext(
context.registries(), context.generator(), context.generator().getBiomeSource(),
context.structureState().randomState(), context.templates(), context.structureState().getLevelSeed(),
context.levelKey(), chunk, biome -> true, context.generator().getSeaLevel(),
policies.injection()::surfaceHeight), holder, plan, references);
if (!generated.isValid()) {
if (replace) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
}
policies.injection().discard(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
continue;
}
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(generated, structure,
plan.terrain(), plan.structureKey());
policies.injection().record(plan, NativeStructureOwnershipFingerprint.capture(plan.structureKey(), generated, envelope));
O recorded;
try {
recorded = policies.references().findPersisted(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
if (!NativeStructureOwnershipFingerprint.matches(recorded, generated)) {
throw new IllegalStateException("Configured structure ownership was not retained: " + plan.structureKey());
}
chunk.setStartForStructure(structure, generated);
} catch (RuntimeException | Error failure) {
try {
policies.injection().discard(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
} catch (RuntimeException | Error cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
throw failure;
}
ownership.put(structure, recorded);
}
return ownership;
}
private static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> Map<Structure, StructureTerrainSettings> adjust(
NativeContext context, Policies<P, O> policies, ProtoChunk chunk, Map<Structure, O> ownership) {
Registry<Structure> registry = context.registries().lookupOrThrow(Registries.STRUCTURE);
Map<Structure, StructureTerrainSettings> terrain = new HashMap<>();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
if (!start.isValid()) {
continue;
}
String key = Objects.requireNonNull(registry.getKey(structure), "structure key").toString();
if (!policies.allowsFootprint().test(start.getBoundingBox())) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
if (ownership.remove(structure) != null) {
policies.references().discard(key, chunk.getPos().x(), chunk.getPos().z());
}
continue;
}
O recorded = ownership.get(structure);
if (recorded != null) {
terrain.put(structure, NativeStructureTerrainIntegrator.resolveNativeTerrain(start, recorded.terrain()));
continue;
}
boolean underground = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
StructurePlacementDecision decision = policies.references().resolve(key, underground);
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
NativeStructureVerticalPlacer.applyVerticalPlacement(start, key, decision.yShift(),
context.generator().getSeaLevel(), chunk.getMinY(), chunk.getMinY() + chunk.getHeight(),
underground, decision.preserveSourceY(), decision.yBand(), policies.injection()::surfaceHeight);
StructureTerrainSettings resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(start, structure,
start.getReferences(), resolved, key);
chunk.setStartForStructure(structure, wrapped);
if (wrapped.isValid()) {
terrain.put(structure, resolved);
}
}
return terrain;
}
static void generateNaturalStarts(NativeContext context, ChunkAccess chunk) {
if (!context.enabled() || SharedConstants.DEBUG_DISABLE_STRUCTURES) {
return;
}
RandomState random = context.structureState().randomState();
Climate.Sampler climate = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
for (Holder<StructureSet> holder : context.structureState().possibleStructureSets()) {
generateSet(context, chunk, holder.value(), climate);
}
}
private static void generateSet(NativeContext context, ChunkAccess chunk, StructureSet set, Climate.Sampler climate) {
List<StructureSet.StructureSelectionEntry> entries = set.structures();
for (StructureSet.StructureSelectionEntry entry : entries) {
StructureStart existing = chunk.getStartForStructure(entry.structure().value());
if (existing != null && existing.isValid()) {
return;
}
}
StructurePlacement placement = set.placement();
ChunkPos position = chunk.getPos();
ResourceKey<StructureSet> key = placement instanceof ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyed
? keyed.key : null;
if (!placement.isStructureChunk(context.structureState(), position.x(), position.z(), key)) {
return;
}
if (entries.size() == 1) {
generate(context, chunk, entries.getFirst(), climate);
return;
}
ArrayList<StructureSet.StructureSelectionEntry> remaining = new ArrayList<>(entries);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L));
random.setLargeFeatureSeed(context.structureState().getLevelSeed(), position.x(), position.z());
int weight = 0;
for (StructureSet.StructureSelectionEntry entry : remaining) {
weight += entry.weight();
}
while (!remaining.isEmpty()) {
int selection = random.nextInt(weight);
int index = 0;
for (StructureSet.StructureSelectionEntry entry : remaining) {
selection -= entry.weight();
if (selection < 0) {
break;
}
index++;
}
StructureSet.StructureSelectionEntry selected = remaining.get(index);
if (generate(context, chunk, selected, climate)) {
return;
}
remaining.remove(index);
weight -= selected.weight();
}
}
private static boolean generate(NativeContext context, ChunkAccess chunk,
StructureSet.StructureSelectionEntry entry, Climate.Sampler climate) {
Structure structure = entry.structure().value();
StructureStart existing = chunk.getStartForStructure(structure);
int references = existing == null ? 0 : existing.getReferences();
StructureStart generated = structure.generate(entry.structure(), context.levelKey(), context.registries(),
context.generator(), context.generator().getBiomeSource(), climate, context.structureState().randomState(),
context.templates(), context.structureState().getLevelSeed(), chunk.getPos(), references, chunk,
structure.biomes()::contains);
if (!generated.isValid()) {
return false;
}
chunk.setStartForStructure(structure, generated);
return true;
}
record NativeContext(RegistryAccess registries, ChunkGenerator generator,
ChunkGeneratorStructureState structureState, StructureTemplateManager templates,
ResourceKey<Level> levelKey, WorldOptions worldOptions, boolean adapterEnabled) {
NativeContext {
Objects.requireNonNull(registries, "registries");
Objects.requireNonNull(generator, "generator");
Objects.requireNonNull(structureState, "structureState");
Objects.requireNonNull(templates, "templates");
Objects.requireNonNull(levelKey, "levelKey");
Objects.requireNonNull(worldOptions, "worldOptions");
if (worldOptions.seed() != structureState.getLevelSeed()) {
throw new IllegalArgumentException("Structure state seed differs from world options");
}
}
boolean enabled() {
return worldOptions.generateStructures() && adapterEnabled;
}
}
record Policies<P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>>(
StructureInjectionPolicy<P> injection, StructureReferencePolicy<P, O> references,
Predicate<BoundingBox> allowsFootprint) {
Policies {
Objects.requireNonNull(injection, "injection");
Objects.requireNonNull(references, "references");
Objects.requireNonNull(allowsFootprint, "allowsFootprint");
}
}
record PlannedChunk<O extends StructureOwnershipRecordView<O>>(NativeContext context, ProtoChunk chunk,
Map<Structure, O> ownership, Map<Structure, StructureTerrainSettings> terrain) {
}
}
@@ -0,0 +1,159 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
import art.arcane.iris.structure.nativegen.IrisStructureVolumePolicy;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeRegistryImpl;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVolumeSource;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.WorldOptions;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spigotmc.SpigotWorldConfig;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
final class HeadlessTerrainContext implements AutoCloseable {
private final Engine engine;
private final LevelStorageSource.LevelStorageAccess storage;
private final HeadlessStructurePlanner.NativeContext structures;
private final HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> policies;
private final BukkitBiomePolicy<Holder<Biome>, Biome> biomes;
private final HeadlessChunkSerialization.Context serialization;
private final NativeRegionTerrainWriter writer;
private HeadlessTerrainContext(Options options, LevelStorageSource.LevelStorageAccess storage) {
engine = options.engine();
this.storage = storage;
HeadlessNativeRuntime runtime = options.runtime();
writer = runtime.writer();
HeadlessChunkGenerator generator = new HeadlessChunkGenerator(new HeadlessChunkGenerator.Options(
engine, runtime.registries()));
long seed = engine.getSeedManager().getSeed();
NoiseGeneratorSettings settings = runtime.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(options.noiseSettings()).value();
RandomState random = RandomState.create(runtime.registries().lookupOrThrow(Registries.NOISE), seed, settings);
ChunkGeneratorStructureState state = generator.createState(
runtime.registries().lookupOrThrow(Registries.STRUCTURE_SET), random, seed, options.configuration());
initializeStructureState(state);
StructureTemplateManager templates = new StructureTemplateManager(runtime.resources(), storage,
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
structures = new HeadlessStructurePlanner.NativeContext(runtime.registries(), generator, state,
templates, options.levelKey(), new WorldOptions(seed, true, false), true);
IrisStructurePolicy policy = new IrisStructurePolicy(engine);
policies = new HeadlessStructurePlanner.Policies<>(policy, policy,
bounds -> engine.getComplex().allowsNewGenerationFootprint(
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
NativeBiomeRegistryImpl registry = new NativeBiomeRegistryImpl(runtime.registries().lookupOrThrow(Registries.BIOME));
biomes = new BukkitBiomePolicy<>(new BukkitBiomePolicy.RuntimeOptions<>(seed, engine, null, () -> registry), registry);
serialization = new HeadlessChunkSerialization.Context(new StructurePieceSerializationContext(
runtime.resources(), runtime.registries(), templates), PalettedContainerFactory.create(runtime.registries()), 0L);
NativeStructureVolumeSource<Engine, NativeStructureStartPlan> volumes = new NativeStructureVolumeSource<>(
new NativeStructureVolumeSource.Context(runtime.registries(), templates, options.levelKey(),
LevelHeightAccessor.create(engine.getMinHeight(), engine.getHeight()),
() -> generator, generator::getBiomeSource, () -> state), IrisStructureVolumePolicy.INSTANCE);
NativeStructureVolumeIndex.install(engine, volumes::volumesAt);
}
static HeadlessTerrainContext create(Options options) throws IOException {
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(options.templateStorage())
.createAccess("native-templates");
try {
return new HeadlessTerrainContext(options, storage);
} catch (RuntimeException | Error failure) {
try {
storage.close();
} catch (IOException closeFailure) {
failure.addSuppressed(closeFailure);
}
throw failure;
}
}
private static void initializeStructureState(ChunkGeneratorStructureState state) {
state.ensureStructuresGenerated();
Throwable failure = null;
for (Holder<StructureSet> structureSet : state.possibleStructureSets()) {
if (!(structureSet.value().placement() instanceof ConcentricRingsStructurePlacement rings)) {
continue;
}
try {
state.getRingPositionsFor(rings);
} catch (RuntimeException | Error problem) {
if (failure == null) {
failure = problem;
} else {
failure.addSuppressed(problem);
}
}
}
if (failure instanceof RuntimeException exception) {
throw exception;
}
if (failure instanceof Error error) {
throw error;
}
}
@Override
public void close() throws IOException {
NativeStructureVolumeIndex.uninstall(engine);
storage.close();
}
Engine engine() {
return engine;
}
HeadlessStructurePlanner.NativeContext structures() {
return structures;
}
HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> policies() {
return policies;
}
BukkitBiomePolicy<Holder<Biome>, Biome> biomes() {
return biomes;
}
HeadlessChunkSerialization.Context serialization() {
return serialization;
}
NativeRegionTerrainWriter writer() {
return writer;
}
record Options(Engine engine, HeadlessNativeRuntime runtime, Path templateStorage, ResourceKey<Level> levelKey,
ResourceKey<NoiseGeneratorSettings> noiseSettings, SpigotWorldConfig configuration) {
Options {
Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(runtime, "runtime");
Objects.requireNonNull(templateStorage, "templateStorage");
Objects.requireNonNull(levelKey, "levelKey");
Objects.requireNonNull(noiseSettings, "noiseSettings");
Objects.requireNonNull(configuration, "configuration");
}
}
}
@@ -0,0 +1,56 @@
package art.arcane.iris.probe;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.ChunkStep;
import java.util.ArrayList;
import java.util.List;
final class NativeRegionPlan {
private NativeRegionPlan() {
}
static List<PlannedChunk> plan(int chunkX, int chunkZ, int width) {
if (width < 1 || width > 32) {
throw new IllegalArgumentException("Native terrain tile width must be 1..32 chunks");
}
ChunkStep target = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
int halo = target.accumulatedDependencies().size() - 1;
int maximumX = Math.addExact(chunkX, width - 1);
int maximumZ = Math.addExact(chunkZ, width - 1);
if (!new ChunkPos(Math.subtractExact(chunkX, halo), Math.subtractExact(chunkZ, halo)).isValid()
|| !new ChunkPos(Math.addExact(maximumX, halo), Math.addExact(maximumZ, halo)).isValid()) {
throw new IllegalArgumentException("Native terrain tile exceeds supported chunk bounds");
}
List<PlannedChunk> result = new ArrayList<>((width + halo * 2) * (width + halo * 2));
for (int z = Math.subtractExact(chunkZ, halo); z <= Math.addExact(maximumZ, halo); z++) {
for (int x = Math.subtractExact(chunkX, halo); x <= Math.addExact(maximumX, halo); x++) {
int distance = Math.max(distance(x, chunkX, maximumX), distance(z, chunkZ, maximumZ));
ChunkStatus required = distance == 0 ? ChunkStatus.FULL : target.accumulatedDependencies().get(distance);
ChunkStatus status = required.isAfter(ChunkStatus.TERRAIN) ? ChunkStatus.TERRAIN : required;
result.add(new PlannedChunk(x, z, status, distance == 0));
}
}
return List.copyOf(result);
}
static int terrainHalo() {
ChunkStep target = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
int radius = 0;
for (int distance = 1; distance < target.accumulatedDependencies().size(); distance++) {
if (target.accumulatedDependencies().get(distance).isOrAfter(ChunkStatus.TERRAIN)) {
radius = distance;
}
}
return radius;
}
private static int distance(int value, int minimum, int maximum) {
return Math.max(0, Math.max(minimum - value, value - maximum));
}
record PlannedChunk(int x, int z, ChunkStatus status, boolean target) {
}
}
@@ -0,0 +1,223 @@
package art.arcane.iris.probe;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.Holder;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
final class NativeRegionTerrainWriter {
private final Registry<Biome> biomes;
private final RegistryAccess registries;
private final PalettedContainerFactory containers;
private final Map<String, BlockState> tileStates = new ConcurrentHashMap<>();
private final Map<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
NativeRegionTerrainWriter(RegistryAccess registries) {
this.registries = Objects.requireNonNull(registries, "registries");
biomes = registries.lookupOrThrow(Registries.BIOME);
containers = PalettedContainerFactory.create(registries);
}
RegistryAccess registries() {
return registries;
}
CompoundTag encode(TerrainInput input) {
TerrainData terrain = terrain(input, null);
SerializableChunkData data = new SerializableChunkData(containers,
new ChunkPos(input.chunkX(), input.chunkZ()), input.minimumY() >> 4, 0L, 0L,
ChunkStatus.EMPTY, null, null, UpgradeData.EMPTY, Map.of(),
new ChunkAccess.PackedTicks(List.of(), List.of()), new ShortList[input.height() >> 4], false,
terrain.sections(), List.of(), terrain.blockEntities(), new CompoundTag(), null);
CompoundTag tag = data.write();
tag.putString("iris:checkpoint", "iris:terrain_checkpoint");
return tag;
}
void fill(ProtoChunk chunk, TerrainInput input) {
if (chunk.getPos().x() != input.chunkX() || chunk.getPos().z() != input.chunkZ()
|| chunk.getMinY() != input.minimumY() || chunk.getHeight() != input.height()) {
throw new IllegalArgumentException("Generated terrain does not match its planned chunk");
}
TerrainData terrain = terrain(input, chunk.getSections());
for (int i = 0; i < terrain.sections().size(); i++) {
chunk.getSections()[i] = terrain.sections().get(i).chunkSection();
}
for (CompoundTag entity : terrain.blockEntities()) {
chunk.setBlockEntityNbt(entity);
}
chunk.markUnsaved();
}
private TerrainData terrain(TerrainInput input, LevelChunkSection[] existingBiomes) {
Int2ObjectOpenHashMap<TileInput> tiles = new Int2ObjectOpenHashMap<>(input.tiles().size());
for (TileInput tile : input.tiles()) {
if (tile.y() >= input.height() || tiles.put(tile.x() | tile.z() << 4 | tile.y() << 8, tile) != null) {
throw new IllegalArgumentException("Duplicate or out-of-bounds tile data");
}
}
List<CompoundTag> blockEntities = new ArrayList<>();
List<SerializableChunkData.SectionData> sections = new ArrayList<>(input.height() >> 4);
for (int sectionY = 0; sectionY < input.height(); sectionY += 16) {
PalettedContainer<BlockState> blocks = containers.createForBlockStates();
PalettedContainer<Holder<Biome>> sectionBiomes = existingBiomes == null
? containers.createForBiomes() : existingBiomes[sectionY >> 4].getBiomes().copy();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
BlockState state = Objects.requireNonNull(input.blocks().state(x, sectionY + y, z), "block state");
TileInput tile = tiles.isEmpty() ? null : tiles.get(x | z << 4 | (sectionY + y) << 8);
if (tile == null && state == Blocks.AIR.defaultBlockState()) {
continue;
}
if (tile != null) {
state = tileStates.computeIfAbsent(tile.stateKey(), NativeRegionTerrainWriter::resolveState);
}
blocks.set(x, y, z, state);
if (state.hasBlockEntity()) {
BlockPos position = new BlockPos((input.chunkX() << 4) + x,
input.minimumY() + sectionY + y, (input.chunkZ() << 4) + z);
CompoundTag entity = encodeBlockEntity(state, position, tile);
if (entity != null) {
blockEntities.add(entity);
}
} else if (tile != null) {
throw new IllegalArgumentException("Tile data has no native block entity at " + x + "," + y + "," + z);
}
}
}
}
if (existingBiomes == null) {
for (int y = 0; y < 4; y++) {
for (int z = 0; z < 4; z++) {
for (int x = 0; x < 4; x++) {
String key = Objects.requireNonNull(input.biomes().key(x << 2, sectionY + (y << 2), z << 2), "biome key");
sectionBiomes.set(x, y, z, biomeHolders.computeIfAbsent(key, this::resolveBiome));
}
}
}
}
sections.add(new SerializableChunkData.SectionData((input.minimumY() + sectionY) >> 4,
new LevelChunkSection(blocks, sectionBiomes), null, null));
}
return new TerrainData(sections, blockEntities);
}
private CompoundTag encodeBlockEntity(BlockState state, BlockPos position, TileInput tile) {
BlockEntity entity = ((EntityBlock) state.getBlock()).newBlockEntity(position, state);
if (entity == null) {
if (tile != null) {
throw new IllegalStateException("Native block did not create a tile entity at " + position);
}
return null;
}
if (tile != null) {
CompoundTag merged = entity.saveWithoutMetadata(registries);
merged.merge(tile.payload());
entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, merged));
}
return entity.saveWithFullMetadata(registries);
}
BlockState readBlock(CompoundTag tag, int x, int y, int z) {
for (CompoundTag section : tag.getListOrEmpty("sections").compoundStream().toList()) {
if (section.getByteOr("Y", (byte) 0) == y >> 4) {
return containers.blockStatesContainerCodec().parse(NbtOps.INSTANCE,
section.getCompoundOrEmpty("block_states")).getOrThrow().get(x & 15, y & 15, z & 15);
}
}
throw new IllegalArgumentException("Missing section at " + y);
}
static void write(RegionFile region, CompoundTag tag) throws IOException {
ChunkPos position = new ChunkPos(tag.getIntOr("xPos", 0), tag.getIntOr("zPos", 0));
try (DataOutputStream output = region.getChunkDataOutputStream(position)) {
NbtIo.write(tag, output);
}
}
private static BlockState resolveState(String key) {
try {
return BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, key, false).blockState();
} catch (CommandSyntaxException failure) {
throw new IllegalArgumentException("Unsupported native block state: " + key, failure);
}
}
private Holder<Biome> resolveBiome(String key) {
return biomes.get(ResourceKey.create(Registries.BIOME, Identifier.parse(key)))
.orElseThrow(() -> new IllegalArgumentException("Unsupported native biome: " + key));
}
record TerrainInput(int chunkX, int chunkZ, int minimumY, int height, BlockLookup blocks, KeyLookup biomes, List<TileInput> tiles) {
TerrainInput {
if ((minimumY & 15) != 0 || (height & 15) != 0 || height < 16
|| minimumY < -2032 || (long) minimumY + height > 2032) {
throw new IllegalArgumentException("Terrain bounds must be section aligned within the native height range");
}
Objects.requireNonNull(blocks, "blocks");
Objects.requireNonNull(biomes, "biomes");
tiles = List.copyOf(tiles);
}
}
record TileInput(int x, int y, int z, String stateKey, CompoundTag payload) {
TileInput {
if (x < 0 || x > 15 || z < 0 || z > 15 || y < 0) {
throw new IllegalArgumentException("Tile position is outside its chunk");
}
Objects.requireNonNull(stateKey, "stateKey");
payload = Objects.requireNonNull(payload, "payload").copy();
}
}
private record TerrainData(List<SerializableChunkData.SectionData> sections, List<CompoundTag> blockEntities) {
}
@FunctionalInterface
interface BlockLookup {
BlockState state(int x, int y, int z);
}
@FunctionalInterface
interface KeyLookup {
String key(int x, int y, int z);
}
}
@@ -0,0 +1,264 @@
package art.arcane.iris.probe;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import org.spigotmc.SpigotWorldConfig;
import org.bukkit.NamespacedKey;
import org.bukkit.Bukkit;
import org.bukkit.persistence.PersistentDataType;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
public final class OfflineRegionGenerator {
private OfflineRegionGenerator() {
}
public static void main(String[] arguments) {
PrintStream diagnostics = System.err;
try {
execute(arguments);
} catch (Throwable failure) {
failure.printStackTrace(diagnostics);
diagnostics.flush();
System.exit(1);
}
}
private static void execute(String[] arguments) throws Exception {
if (arguments.length == 1 && arguments[0].equals("--help")) {
System.out.println("Usage: iris-offline pack dimension seed chunkX chunkZ width regionsX regionsZ parallelism spigot worldName levelKey noiseSettings output");
System.out.println("Java 25; width 1..32 chunks; parallelism 1..32. Output must be a new absolute directory.");
System.out.println("Generates fresh TERRAIN checkpoints and immutable Iris history. Native server completion is required for features, population and lighting.");
return;
}
if (Bukkit.getServer() != null) {
throw new IllegalStateException("Offline generation requires a separate process without a running Bukkit server");
}
long processStarted = System.nanoTime();
HeadlessNativeBootstrap.initialize();
Configuration configuration = Configuration.parse(arguments);
Files.createDirectory(configuration.output());
force(configuration.output());
try (FileChannel channel = FileChannel.open(configuration.output().resolve("checkpoint.lock"),
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
FileLock lock = channel.lock()) {
if (!lock.isValid()) {
throw new IllegalStateException("Headless checkpoint lock is invalid");
}
run(configuration);
System.out.println("processSeconds=" + (System.nanoTime() - processStarted) / 1_000_000_000.0);
}
}
private static void run(Configuration configuration) throws Exception {
Path staged = Files.createDirectory(configuration.output().resolve("incomplete"));
byte[] configurationBytes = Files.readAllBytes(configuration.spigot());
Path capturedConfiguration = staged.resolve("spigot.yml");
Files.write(capturedConfiguration, configurationBytes, StandardOpenOption.CREATE_NEW);
String configurationHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(configurationBytes));
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime(staged);
long started = System.nanoTime();
long writes = 0;
long terrainWrites = 0;
double generationSeconds = 0;
double serializationSeconds = 0;
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(configuration.pack(),
configuration.dimension(), "[headless-native-region]", runtime, staged));
RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "native-region")) {
session.retainWorld(staged.resolve("world"));
SpigotWorldConfig spigot = HeadlessStructureConfiguration.load(new HeadlessStructureConfiguration.Options(
capturedConfiguration, configuration.worldName(), configuration.levelKey(), staged.resolve("configuration")));
PregenPerformanceProfile.apply(session.engine());
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
: hydrology.preparePregeneration(configuration.hydrologyArea());
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
configuration.noiseSettings(), spigot));
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
RegionGenerationWindow.Policy.STANDALONE)) {
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
Files.createDirectory(regionDirectory);
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
context, regionDirectory, workers));
for (int row = 0; row < configuration.regionsZ(); row++) {
for (int column = 0; column < configuration.regionsX(); column++) {
int regionX = row % 2 == 0 ? column : configuration.regionsX() - column - 1;
int chunkX = Math.addExact(configuration.chunkX(), Math.multiplyExact(regionX, configuration.width()));
int chunkZ = Math.addExact(configuration.chunkZ(), Math.multiplyExact(row, configuration.width()));
RegionResult generated = generateRegion(new RegionRequest(context, rolling, writer, chunkX, chunkZ,
configuration.width(), configuration.parallelism(),
staged.resolve("receipts.properties")));
writes += generated.written().chunks();
terrainWrites += generated.written().terrainChunks();
generationSeconds += generated.generationSeconds();
serializationSeconds += generated.serializationSeconds();
System.out.println("region=" + regionX + "," + row + " residentChunks=" + generated.residentChunks()
+ " upgradedChunks=" + generated.upgradedChunks() + " writtenChunks=" + generated.written().chunks());
}
}
} finally {
PregenPerformanceProfile.restore();
}
}
double wallSeconds = (System.nanoTime() - started) / 1_000_000_000.0;
String report = String.format(Locale.ROOT,
"format=iris-native-terrain-checkpoint\nplatform=headless-native\nstatus=minecraft:terrain\ncomplete=false\n"
+ "targetChunks=%d\nchunkWrites=%d\nterrainWrites=%d\nparallelism=%d\nseed=%d\nchunkX=%d\nchunkZ=%d\nwidth=%d\nregionsX=%d\nregionsZ=%d\n"
+ "levelKey=%s\nworldName=%s\nnoiseSettings=%s\nspigotSha256=%s\npipelineWallSeconds=%.3f\ngenerationSeconds=%.3f\nserializationSeconds=%.3f\n"
+ "limitations=Fresh offline world checkpoint only. Native features, population, lighting, POI, entities and scheduled ticks require native completion. No live plugin events are dispatched. Authored tile payloads remain in retained mantle for native completion. No transitions or existing chunks are imported.\n",
Math.multiplyExact(Math.multiplyExact(configuration.width(), configuration.width()),
Math.multiplyExact(configuration.regionsX(), configuration.regionsZ())), writes, terrainWrites,
configuration.parallelism(), configuration.seed(), configuration.chunkX(), configuration.chunkZ(),
configuration.width(), configuration.regionsX(), configuration.regionsZ(), configuration.levelKey().identifier(),
configuration.worldName(), configuration.noiseSettings().identifier(), configurationHash,
wallSeconds, generationSeconds, serializationSeconds);
Files.writeString(staged.resolve("checkpoint.properties"), report, StandardOpenOption.CREATE_NEW);
long publicationStarted = System.nanoTime();
publish(staged, configuration.output().resolve("native-terrain-checkpoint"));
System.out.println(report);
System.out.println("publicationSeconds=" + (System.nanoTime() - publicationStarted) / 1_000_000_000.0);
System.out.println("totalSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
}
private static RegionResult generateRegion(RegionRequest request) throws Exception {
long generationStarted = System.nanoTime();
HeadlessRegionTerrain.Result generated = request.rolling().generate(new HeadlessRegionTerrain.Request(
request.context(), request.chunkX(), request.chunkZ(), request.width(), request.parallelism()), System.out::println);
double generationSeconds = (System.nanoTime() - generationStarted) / 1_000_000_000.0;
long serializationStarted = System.nanoTime();
OfflineRegionWriter.Result written = request.writer().write(generated);
writeReceipts(generated, request.receipts());
return new RegionResult(written, generated.chunks().size(), generated.updated().size(), generationSeconds,
(System.nanoTime() - serializationStarted) / 1_000_000_000.0);
}
private record RegionRequest(HeadlessTerrainContext context, HeadlessRegionTerrain.Session rolling, OfflineRegionWriter writer,
int chunkX, int chunkZ, int width, int parallelism, Path receipts) {
}
private record RegionResult(OfflineRegionWriter.Result written, int residentChunks, int upgradedChunks,
double generationSeconds, double serializationSeconds) {
}
private static void writeReceipts(HeadlessRegionTerrain.Result generated, Path path) throws Exception {
NamespacedKey key = new NamespacedKey("iris", "natural_terrain");
MessageDigest digest = MessageDigest.getInstance("SHA-256");
StringBuilder receipts = new StringBuilder();
for (ProtoChunk chunk : generated.updated()) {
if (!chunk.getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
continue;
}
byte[] receipt = chunk.persistentDataContainer.get(key, PersistentDataType.BYTE_ARRAY);
if (receipt == null) {
throw new IllegalStateException("Native TERRAIN checkpoint has no receipt at " + chunk.getPos());
}
receipts.append(chunk.getPos().x()).append(',').append(chunk.getPos().z()).append('=')
.append(HexFormat.of().formatHex(digest.digest(receipt))).append('\n');
}
Files.writeString(path, receipts, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
private static void forceTree(Path directory) throws IOException {
Files.walkFileTree(directory, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
force(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path path, IOException failure) throws IOException {
if (failure != null) {
throw failure;
}
force(path);
return FileVisitResult.CONTINUE;
}
});
}
static void publish(Path staged, Path destination) throws IOException {
forceTree(staged);
Files.move(staged, destination, StandardCopyOption.ATOMIC_MOVE);
force(destination.getParent());
}
private static void force(Path path) throws IOException {
try (FileChannel channel = FileChannel.open(path, Files.isDirectory(path)
? StandardOpenOption.READ : StandardOpenOption.WRITE)) {
channel.force(true);
}
}
record Configuration(File pack, String dimension, long seed, int chunkX, int chunkZ, int width,
int regionsX, int regionsZ, int parallelism, Path spigot, String worldName, ResourceKey<Level> levelKey,
ResourceKey<NoiseGeneratorSettings> noiseSettings, Path output) {
Configuration {
if (regionsX < 1 || regionsZ < 1) {
throw new IllegalArgumentException("Region dimensions must be positive");
}
Math.multiplyExact(Math.multiplyExact(width, width), Math.multiplyExact(regionsX, regionsZ));
NativeRegionPlan.plan(chunkX, chunkZ, width);
NativeRegionPlan.plan(Math.addExact(chunkX, Math.multiplyExact(regionsX - 1, width)),
Math.addExact(chunkZ, Math.multiplyExact(regionsZ - 1, width)), width);
if (parallelism < 1 || parallelism > 32 || !Files.isRegularFile(spigot)) {
throw new IllegalArgumentException("Parallelism must be 1..32 and Spigot configuration must exist");
}
if (!output.isAbsolute() || Files.exists(output) || !Files.isDirectory(output.getParent())) {
throw new IllegalArgumentException("Output must be a new absolute directory under an existing parent");
}
}
HydrologyTileCache.PregenerationArea hydrologyArea() {
int halo = NativeRegionPlan.terrainHalo();
long minimumX = ((long) chunkX - halo) * 16L;
long minimumZ = ((long) chunkZ - halo) * 16L;
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
}
static Configuration parse(String[] arguments) throws IOException {
if (arguments.length != 14) {
throw new IllegalArgumentException("Expected pack dimension seed chunkX chunkZ width regionsX regionsZ parallelism spigot worldName levelKey noiseSettings output");
}
return new Configuration(new File(arguments[0]).getCanonicalFile(), arguments[1], Long.parseLong(arguments[2]),
Integer.parseInt(arguments[3]), Integer.parseInt(arguments[4]), Integer.parseInt(arguments[5]),
Integer.parseInt(arguments[6]), Integer.parseInt(arguments[7]), Integer.parseInt(arguments[8]),
Path.of(arguments[9]), arguments[10],
ResourceKey.create(Registries.DIMENSION, Identifier.parse(arguments[11])),
ResourceKey.create(Registries.NOISE_SETTINGS, Identifier.parse(arguments[12])), Path.of(arguments[13]));
}
}
}
@@ -0,0 +1,161 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.IORunnable;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class OfflineRegionWriter {
private static final int MAXIMUM_PREPARATION_WORKERS = 8;
private static final int MAXIMUM_STATUS_REGIONS = 64;
private final Options options;
private final Map<Long, int[]> published = new LinkedHashMap<>(MAXIMUM_STATUS_REGIONS, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, int[]> eldest) {
return size() > MAXIMUM_STATUS_REGIONS;
}
};
OfflineRegionWriter(Options options) {
this.options = Objects.requireNonNull(options, "options");
}
Result write(HeadlessRegionTerrain.Result generated) throws Exception {
options.workers().requireOpen();
try (OpenRegions regions = new OpenRegions(options)) {
List<ChunkWrite> pending = new ArrayList<>(generated.updated().size());
for (ProtoChunk chunk : generated.updated()) {
ChunkPos position = chunk.getPos();
long regionKey = ChunkPos.pack(position.x() >> 5, position.z() >> 5);
int[] statuses = published.computeIfAbsent(regionKey, ignored -> new int[1024]);
int index = (position.x() & 31) + ((position.z() & 31) << 5);
int status = chunk.getPersistedStatus().getIndex() + 2;
if (statuses[index] >= status) {
continue;
}
RegionFile region = regions.get(position);
if (statuses[index] == 0) {
statuses[index] = readStatus(region, position);
if (statuses[index] >= status) {
continue;
}
}
pending.add(new ChunkWrite(chunk, region, statuses, index, status));
}
if (pending.isEmpty()) {
return new Result(0, 0);
}
int[] counts = new int[2];
options.workers().process(new RegionGenerationWindow.Request<>(pending.size(),
Math.min(options.workers().parallelism(), MAXIMUM_PREPARATION_WORKERS),
index -> prepare(pending.get(index)),
(index, prepared) -> {
ChunkWrite chunk = prepared.chunk();
prepared.commit().run(chunk.region());
chunk.statuses()[chunk.index()] = chunk.status();
counts[0]++;
if (chunk.chunk().getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
counts[1]++;
}
}));
return new Result(counts[0], counts[1]);
}
}
private PreparedWrite prepare(ChunkWrite chunk) throws IOException {
CompoundTag tag = HeadlessChunkSerialization.copyOf(chunk.chunk(), options.context().serialization()).write();
WriteData prepared = chunk.region().moonrise$startWrite(tag, chunk.chunk().getPos());
try (DataOutputStream output = prepared.output()) {
NbtIo.write(tag, output);
}
return new PreparedWrite(chunk, prepared.write());
}
private static int readStatus(RegionFile region, ChunkPos position) throws IOException {
try (DataInputStream input = region.getChunkDataInputStream(position)) {
if (input == null) {
return 1;
}
ChunkStatus status = ChunkStatus.byName(NbtIo.read(input).getStringOr("Status", ""));
if (status == null) {
throw new IOException("Invalid native chunk status at " + position);
}
return status.getIndex() + 2;
}
}
record Options(HeadlessTerrainContext context, Path directory, RegionGenerationWindow workers) {
Options {
Objects.requireNonNull(context, "context");
Objects.requireNonNull(directory, "directory");
Objects.requireNonNull(workers, "workers");
}
}
record Result(int chunks, int terrainChunks) {
}
private record ChunkWrite(ProtoChunk chunk, RegionFile region, int[] statuses, int index, int status) {
}
private record PreparedWrite(ChunkWrite chunk, IORunnable commit) {
}
private static final class OpenRegions implements AutoCloseable {
private final Options options;
private final Map<Long, RegionFile> regions = new HashMap<>();
private OpenRegions(Options options) {
this.options = options;
}
private RegionFile get(ChunkPos position) throws IOException {
long key = ChunkPos.pack(position.x() >> 5, position.z() >> 5);
RegionFile region = regions.get(key);
if (region != null) {
return region;
}
Path path = options.directory().resolve("r." + (position.x() >> 5) + "." + (position.z() >> 5) + ".mca");
region = new RegionFile(new RegionStorageInfo("headless-native-terrain",
options.context().structures().levelKey(), "chunk"), path, options.directory(), false);
regions.put(key, region);
return region;
}
@Override
public void close() throws IOException {
IOException failure = null;
for (RegionFile region : regions.values()) {
try {
region.close();
} catch (IOException exception) {
if (failure == null) {
failure = exception;
} else {
failure.addSuppressed(exception);
}
}
}
if (failure != null) {
throw failure;
}
}
}
}
@@ -0,0 +1,176 @@
package art.arcane.iris.probe;
import java.time.Duration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class RegionGenerationWindow implements AutoCloseable {
private static final int UNRESPONSIVE_WORKER_EXIT = 70;
private final int parallelism;
private final Policy policy;
private final ExecutorService workers;
private boolean closed;
private boolean processing;
RegionGenerationWindow(int parallelism, Policy policy) {
if (parallelism < 1 || parallelism > 32) {
throw new IllegalArgumentException("Generation workers must be 1..32");
}
this.parallelism = parallelism;
this.policy = Objects.requireNonNull(policy, "policy");
workers = Executors.newFixedThreadPool(parallelism);
}
@Override
public synchronized void close() {
if (processing) {
throw new IllegalStateException("Cannot close generation workers from an active batch");
}
if (closed) {
return;
}
closed = true;
workers.shutdown();
drain(workers, policy, null);
}
int parallelism() {
return parallelism;
}
synchronized void requireOpen() {
if (closed) {
throw new IllegalStateException("Generation workers are closed");
}
}
synchronized <T> void process(Request<T> request) throws Exception {
requireOpen();
if (processing || request.parallelism() > parallelism) {
throw new IllegalArgumentException("Generation batch requires idle workers within the session limit");
}
ExecutorCompletionService<Completed<T>> completions = new ExecutorCompletionService<>(workers);
Set<Future<Completed<T>>> active = new HashSet<>(request.parallelism());
processing = true;
try {
int submitted = 0;
for (; submitted < Math.min(request.count(), request.parallelism()); submitted++) {
active.add(submit(completions, request.generator(), submitted));
}
for (int completed = 0; completed < request.count(); completed++) {
Future<Completed<T>> pending = completions.poll(policy.completionTimeout().toNanos(), TimeUnit.NANOSECONDS);
if (pending == null) {
throw new TimeoutException("No generation worker completed within " + policy.completionTimeout());
}
active.remove(pending);
Completed<T> result = pending.get();
if (submitted < request.count()) {
active.add(submit(completions, request.generator(), submitted++));
}
request.sink().accept(result.index(), result.value());
}
} catch (Exception | Error failure) {
closed = true;
for (Future<Completed<T>> future : active) {
future.cancel(true);
}
workers.shutdownNow();
drain(workers, policy, failure);
throw failure;
} finally {
processing = false;
}
}
private static void drain(ExecutorService workers, Policy policy, Throwable failure) {
boolean interrupted = Thread.interrupted() || failure instanceof InterruptedException;
long deadline = System.nanoTime() + policy.drainTimeout().toNanos();
try {
while (!workers.isTerminated()) {
long remaining = deadline - System.nanoTime();
if (remaining <= 0L) {
break;
}
try {
workers.awaitTermination(remaining, TimeUnit.NANOSECONDS);
} catch (InterruptedException interruption) {
interrupted = true;
}
}
if (workers.isTerminated()) {
return;
}
IllegalStateException stalled = new IllegalStateException(
"Generation workers did not stop within " + policy.drainTimeout()
+ "; generation resources remain open and no checkpoint will be published", failure);
stalled.printStackTrace(System.err);
if (policy.haltOnUnresponsiveWorkers()) {
System.err.println("Terminating offline generation; incomplete output is retained for inspection");
System.err.flush();
Runtime.getRuntime().halt(UNRESPONSIVE_WORKER_EXIT);
}
System.err.println("Waiting for embedded generation workers to stop before releasing their resources");
while (!workers.isTerminated()) {
try {
workers.awaitTermination(1L, TimeUnit.DAYS);
} catch (InterruptedException interruption) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
private static <T> Future<Completed<T>> submit(ExecutorCompletionService<Completed<T>> completions,
Generator<T> generator, int index) {
return completions.submit(() -> new Completed<>(index, generator.generate(index)));
}
record Policy(Duration completionTimeout, Duration drainTimeout, boolean haltOnUnresponsiveWorkers) {
static final Policy EMBEDDED = new Policy(Duration.ofMinutes(5), Duration.ofSeconds(30), false);
static final Policy STANDALONE = new Policy(Duration.ofMinutes(5), Duration.ofSeconds(30), true);
Policy {
Objects.requireNonNull(completionTimeout, "completionTimeout");
Objects.requireNonNull(drainTimeout, "drainTimeout");
if (completionTimeout.isNegative() || completionTimeout.isZero()
|| drainTimeout.isNegative() || drainTimeout.isZero()) {
throw new IllegalArgumentException("Worker completion and drain timeouts must be positive");
}
completionTimeout.toNanos();
drainTimeout.toNanos();
}
}
record Request<T>(int count, int parallelism, Generator<T> generator, Sink<T> sink) {
Request {
if (count < 1 || count > 4096 || parallelism < 1 || parallelism > 32) {
throw new IllegalArgumentException("Generation window requires 1..4096 tasks and 1..32 workers");
}
}
}
private record Completed<T>(int index, T value) {
}
@FunctionalInterface
interface Generator<T> {
T generate(int index) throws Exception;
}
@FunctionalInterface
interface Sink<T> {
void accept(int index, T value) throws Exception;
}
}
@@ -0,0 +1,144 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
public final class IrisChunkGeneratorHeightCacheTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
}
verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
when(fixture.engine().getCacheID()).thenReturn(2);
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
}
@Test
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.engine(), times(1)).getHeight(0, 0, true);
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
}
@Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
when(policy.openQuery(any())).thenReturn(session);
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null));
}
verify(session, times(6)).resolvedColumn();
}
private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class);
when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
}
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
return generator;
}
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
Field field = NativeChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static TerrainBoundarySignature signature() {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
}
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
}
}
@@ -0,0 +1,129 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeGenerationWriteGuard;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationActivation;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
import java.util.function.Predicate;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public final class NativeGenerationWriteGuardTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
when(objects.contains(-3, 74, -7)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, stack, -64));
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
verifyNoInteractions(stack);
assertFalse(NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, null, -64))
.test(new BlockPos(-3, 11, -7)));
}
@Test
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout negative = mock(DimensionStackLayout.class);
DimensionStackLayout positive = mock(DimensionStackLayout.class);
when(objects.isEmpty()).thenReturn(true);
when(stack.sample(-1, -1)).thenReturn(negative);
when(stack.sample(-1, 1)).thenReturn(positive);
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(GenerationWritePolicy.protectedPositions(objects, stack, -64));
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
verify(stack, times(1)).sample(-1, -1);
verify(stack, times(1)).sample(-1, 1);
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
}
@Test
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationActivation active = mock(GenerationActivation.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(history.activeActivation()).thenReturn(active);
when(active.activationId()).thenReturn(4L);
assertTrue(GenerationWritePolicy.isHistoricalStructure(engine, 3));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 0));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 4));
assertFalse(GenerationWritePolicy.isHistoricalStructure(engine, 5));
}
@Test
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
WorldGenLevel region = mock(WorldGenLevel.class);
ChunkAccess partial = mock(ChunkAccess.class);
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine.getComplex()::allowsMantleChunkWrite, region, new ChunkPos(0, 0)));
ChunkAccess complete = mock(ChunkAccess.class);
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
when(region.getChunk(1, -1)).thenReturn(complete);
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine.getComplex()::allowsMantleChunkWrite, region, new ChunkPos(0, 0)));
}
@Test
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
ChunkAccess chunk = mock(ChunkAccess.class);
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.TERRAIN));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.FEATURES));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.SPAWN));
}
}
@@ -0,0 +1,476 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureStartInjector;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureFactory;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeJigsawMetadata;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.ForcedStructureChunkGenerator;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTemplatePoolBounds;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import java.util.Set;
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFactoryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void genericSourceDoesNotRequireJigsawType() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
Structure configured = NativeStructureFactory.configure(
null, source, null, false, 80);
assertSame(source, configured);
}
@Test
public void jigsawOptionsFailClosedForNonJigsawSource() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
null, source, new IrisJigsawConfiguration(), false, 80));
}
@Test
public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() {
CompoundTag scalar = new CompoundTag();
scalar.putInt("max_distance_from_center", 80);
CompoundTag axisSpecific = new CompoundTag();
CompoundTag distance = new CompoundTag();
distance.putInt("horizontal", 96);
distance.putInt("vertical", 64);
axisSpecific.put("max_distance_from_center", distance);
assertEquals(80, NativeJigsawMetadata.sourceDistance(scalar, "horizontal"));
assertEquals(80, NativeJigsawMetadata.sourceDistance(scalar, "vertical"));
assertEquals(96, NativeJigsawMetadata.sourceDistance(axisSpecific, "horizontal"));
assertEquals(64, NativeJigsawMetadata.sourceDistance(axisSpecific, "vertical"));
}
@Test
public void sourceMetadataIncludesAdjustBoundingBoxExpansion() {
Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty())
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
.build());
assertEquals(0, NativeJigsawMetadata.horizontalReferenceExpansion(none));
assertEquals(12, NativeJigsawMetadata.horizontalReferenceExpansion(beardBox));
}
@Test
public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() {
StructurePoolElement narrow = inlineElement(30, 8, 70);
StructurePoolElement wide = inlineElement(96, 8, 12);
ListPoolElement list = new ListPoolElement(
List.of(narrow, wide, EmptyPoolElement.INSTANCE),
StructureTemplatePool.Projection.RIGID);
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(inlineElement(110, 8, 20), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1)));
startHolder.bind(start);
assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
}
@Test
public void oversizedInlineStartTemplateReportsItsFullSpan() {
BoundPoolHolder holder = new BoundPoolHolder("test:oversized");
StructureTemplatePool pool = new StructureTemplatePool(
holder,
List.of(Pair.of(inlineElement(160, 8, 9), 1)));
holder.bind(pool);
assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() {
BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty");
StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of());
emptyHolder.bind(empty);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1)));
startHolder.bind(start);
BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement");
StructureTemplatePool replacement = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1)));
replacementHolder.bind(replacement);
BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated");
StructureTemplatePool unrelated = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1)));
unrelatedHolder.bind(unrelated);
ResourceKey<StructureTemplatePool> startKey = startHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> replacementKey = replacementHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> childAlias = ResourceKey.create(
Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias"));
List<PoolAliasBinding> aliases = List.of(
PoolAliasBinding.direct(startKey, replacementKey),
PoolAliasBinding.direct(childAlias, unrelatedKey));
Map<ResourceKey<StructureTemplatePool>, StructureTemplatePool> pools = Map.of(
replacementKey, replacement,
unrelatedKey, unrelated);
assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startKey, aliases, pools::get));
}
@Test
public void sourceStartSpanDoesNotIncludeItsFallbackPool() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1)));
startHolder.bind(start);
assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null));
}
@Test
public void customPoolElementUsesItsBoundingBoxCapability() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of());
fallbackHolder.bind(fallback);
BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom");
StructureTemplatePool pool = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1)));
poolHolder.bind(pool);
assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() {
ChunkGenerator delegate = new TestChunkGenerator();
Holder<Biome> biome = Holder.direct((Biome) null);
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
delegate, biome);
assertEquals(63, generator.getSeaLevel());
assertEquals(64, generator.getBaseHeight(
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
NoiseColumn column = generator.getBaseColumn(
0, 0, heightAccessor, null);
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
assertSame(Blocks.STONE, column.getBlock(81).getBlock());
assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource());
}
@Test
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
StructureStart start = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(piece))
);
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
start, source, -20, LevelHeightAccessor.create(-64, 384));
assertNotSame(start, relocated);
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
assertEquals(-20, relocated.getBoundingBox().minY());
}
@Test
public void invalidGeneratedStartIsANormalSkippedCandidate() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
StructureStart valid = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(
new SwampHutPiece(RandomSource.create(23L), 0, 0)))
);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
StructureStart.INVALID_START));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
new StructureStart(source, new ChunkPos(0, 0), 0,
new PiecesContainer(List.of()))));
assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid));
}
@Test
public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0);
boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0);
StructureStart boundary = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(boundaryPiece))
);
SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0);
oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0);
StructureStart oversized = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(oversizedPiece))
);
StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication(
boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content");
StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication(
oversized, source, 0,
new IrisStructureTerrain(),
"test:oversized_content");
assertTrue(boundaryResult.isValid());
assertSame(StructureStart.INVALID_START, result);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result));
assertEquals(1, oversized.getPieces().size());
}
private static final class TestChunkGenerator extends ChunkGenerator {
private TestChunkGenerator() {
super(new FixedBiomeSource(Holder.direct((Biome) null)));
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(this);
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public CompletableFuture<ChunkAccess> buildTerrain(
ChunkAccess centerChunk, Blender blender, RandomState randomState,
StructureManager structureManager, BiomeManager biomeManager, WorldGenRegion region, Set<Holder<Biome>> possibleBiomes) {
return CompletableFuture.completedFuture(centerChunk);
}
@Override
public int getSeaLevel() {
return 63;
}
@Override
public int getMinY() {
return -64;
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor heightAccessor, RandomState randomState) {
return 64;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
RandomState randomState) {
BlockState[] states = new BlockState[heightAccessor.getHeight()];
for (int index = 0; index < states.length; index++) {
states[index] = Blocks.STONE.defaultBlockState();
}
return new NoiseColumn(heightAccessor.getMinY(), states);
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState randomState,
BlockPos feetPos, SamplerContext samplerContext) {
}
}
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
StructureTemplate template = new StructureTemplate();
CompoundTag tag = new CompoundTag();
ListTag size = new ListTag();
size.add(IntTag.valueOf(sizeX));
size.add(IntTag.valueOf(sizeY));
size.add(IntTag.valueOf(sizeZ));
tag.put("size", size);
template.load(BuiltInRegistries.BLOCK, tag);
return new InlinePoolElement(template);
}
private static final class InlinePoolElement extends SinglePoolElement {
private final StructureTemplate inlineTemplate;
private InlinePoolElement(StructureTemplate template) {
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
inlineTemplate = template;
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return inlineTemplate.getBoundingBox(
new StructurePlaceSettings().setRotation(rotation), position);
}
}
private static final class BoundingPoolElement extends StructurePoolElement {
private final int width;
private final int depth;
private BoundingPoolElement(int width, int depth) {
super(StructureTemplatePool.Projection.RIGID);
this.width = width;
this.depth = depth;
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(width, 1, depth);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(
position.getX(), position.getY(), position.getZ(),
position.getX() + width - 1, position.getY(),
position.getZ() + depth - 1);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world,
StructureManager structureManager, ChunkGenerator chunkGenerator,
BlockPos position, BlockPos pivot, Rotation rotation,
BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
}
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
private BoundPoolHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
}
private void bind(StructureTemplatePool pool) {
bindValue(pool);
}
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureGenerationKeys;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import net.minecraft.SharedConstants;
import net.minecraft.core.Vec3i;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureGenerationKeysContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
}
private static RandomSpreadStructurePlacement placement(float frequency) {
return new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
frequency,
1,
Optional.empty(),
2,
1,
RandomSpreadType.LINEAR);
}
}
@@ -0,0 +1,199 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureLocatePersistence;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureLocatePersistenceTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void storageProbeScansEachCandidateOnlyOnce() {
AtomicInteger reads = new AtomicInteger();
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
chunk.putString("Status", "full");
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 0);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
reads.incrementAndGet();
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
ChunkPos chunkPos = new ChunkPos(7, -11);
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertEquals(1, reads.get());
assertEquals(1, budget.used());
}
@Test
public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() {
AtomicBoolean datafixed = new AtomicBoolean();
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
legacy.putString("Status", "full");
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
datafixed.set(true);
CompoundTag fixed = storedChunk.copy();
fixed.putInt("DataVersion",
SharedConstants.getCurrentVersion().dataVersion().version());
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
fixed.put("structures", structures);
return fixed;
});
assertFalse(budget.acceptsStored(
null, new ChunkPos(3, 4), "minecraft:village_plains", true));
assertTrue(datafixed.get());
}
@Test
public void partialDatafixFailureFallsBackToSelectedChunkVerification() {
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
throw new IllegalStateException("broken partial datafix");
});
assertTrue(budget.acceptsStored(
null, new ChunkPos(9, -2), "minecraft:village_plains", true));
}
@Test
public void missingStartsContainerFallsBackToSelectedChunkVerification() {
CompoundTag chunk = currentChunk();
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", false));
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", true));
}
@Test
public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag structures = new CompoundTag();
structures.put("starts", new CompoundTag());
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertFalse(budget.acceptsStored(
null, new ChunkPos(-3, 2), "minecraft:village_plains", false));
}
@Test
public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag tombstone = new CompoundTag();
tombstone.putString("id", "INVALID");
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", tombstone);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(12, -9);
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
@Test
public void malformedNonCompoundStartIsRejectedWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag starts = new CompoundTag();
starts.putInt("minecraft:village_plains", 1);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
assertFalse(budget(chunk).acceptsStored(
null, new ChunkPos(1, 1), "minecraft:village_plains", false));
}
@Test
public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(-4, -7);
assertTrue(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
private static CompoundTag currentChunk() {
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
return chunk;
}
private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) {
return new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
}
}
@@ -0,0 +1,345 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceRepair;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipFingerprint;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.ListTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipFingerprintTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() {
long seed = 8372619L;
ChunkPos origin = new ChunkPos(9, -4);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
String beforeReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", wrapped);
ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null);
PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null);
assertEquals(1, loadedPieces.pieces().size());
assertTrue(loadedPieces.pieces().getFirst()
instanceof OceanMonumentPieces.MonumentBuilding);
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, loadedPieces);
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", reloaded);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", wrapped, NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)), plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()));
assertEquals(beforeReload, afterReload);
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded));
assertEquals(alignedLocatorY, ownership.locatorY());
assertEquals(1, reloaded.getPieces().size());
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void sameKeyAndOriginStillRequiresMatchingContentIdentity() {
long seed = 18273L;
ChunkPos origin = new ChunkPos(2, 3);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", expected, expected.getBoundingBox()), plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()));
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
}
@Test
public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() {
long seed = 772931L;
ChunkPos origin = new ChunkPos(4, -7);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", wrapped, NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)), plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()));
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(wrapped.getPieces()));
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
ChunkPos target = outsideLiveBounds(ownership, reloaded);
assertNotNull(target);
assertTrue(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, ownership));
assertFalse(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, null));
}
@Test
public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos westOrigin = new ChunkPos(0, 0);
ChunkPos eastOrigin = new ChunkPos(16, 0);
StructureStart west = monumentStart(structure, westOrigin, 317L);
StructureStart east = monumentStart(structure, eastOrigin, 941L);
west.getPieces().getFirst().move(112, 0, 0);
east.getPieces().getFirst().move(-96, 0, 0);
NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure);
NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure);
ChunkPos shared = new ChunkPos(8, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects(
NativeStructureReferenceEnvelope.contentBounds(east)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", west, westOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", east, eastOwnership));
}
@Test
public void denseAdjacentManualStartsDoNotSuppressEachOther() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos firstOrigin = new ChunkPos(0, 0);
ChunkPos secondOrigin = new ChunkPos(1, 0);
StructureStart first = monumentStart(structure, firstOrigin, 17L);
StructureStart second = monumentStart(structure, secondOrigin, 23L);
NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure);
NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure);
ChunkPos shared = new ChunkPos(0, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects(
NativeStructureReferenceEnvelope.contentBounds(second)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", first, firstOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", second, secondOwnership));
}
@Test
public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement());
StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement());
assertEquals(
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first),
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded));
}
@Test
public void namedPoolTemplateRemainsPartOfTheStableFingerprint() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructurePoolElement firstElement = StructurePoolElement.single("test:first")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement secondElement = StructurePoolElement.single("test:second")
.apply(StructureTemplatePool.Projection.RIGID);
assertNotEquals(
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, firstElement)),
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, secondElement)));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure,
origin,
0,
new PiecesContainer(List.of(building))
);
}
private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId("monument-test")
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
baseY
);
}
private static NativeStructureOwnershipRecord ownership(
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
return NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
"minecraft:monument", start, NativeStructureReferenceEnvelope.referenceBounds(
start, structure, new IrisStructureTerrain())), plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()));
}
private static StructureStart poolStart(
OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) {
BlockPos position = new BlockPos(
origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ());
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, element, position, 0, Rotation.NONE,
new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(piece)));
}
private static final class IdentityStringPoolElement extends StructurePoolElement {
private IdentityStringPoolElement() {
super(StructureTemplatePool.Projection.RIGID);
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(1, 1, 1);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(position);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world, StructureManager structureManager,
ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot,
Rotation rotation, BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
@Override
public String toString() {
return "IdentityStringPoolElement@"
+ Integer.toHexString(System.identityHashCode(this));
}
}
private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership,
StructureStart start) {
for (int chunkX = ownership.referenceMinChunkX();
chunkX <= ownership.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = ownership.referenceMinChunkZ();
chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) {
ChunkPos candidate = new ChunkPos(chunkX, chunkZ);
if (!start.getBoundingBox().intersects(
candidate.getMinBlockX(), candidate.getMinBlockZ(),
candidate.getMaxBlockX(), candidate.getMaxBlockZ())) {
return candidate;
}
}
}
return null;
}
}
@@ -0,0 +1,327 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.terrain.structure.StructureOwnershipRecordView;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipRecovery;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipFingerprint;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipRecoveryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() {
long seed = 5519231L;
ChunkPos origin = new ChunkPos(7, -5);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
expected, 0, 80, -64, 320);
int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected);
NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24);
StructureStart reloaded = new StructureStart(
structure,
origin,
expected.getReferences(),
OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(expected.getPieces()))
);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, reloaded,
plan, expected, false, NativeStructureOwnershipRecord::capture);
assertNotNull(recovered);
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
recovered.placementIdentity());
assertEquals(expectedLocatorY, recovered.locatorY());
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
recovered.locatorY());
assertEquals(StructureTerrainMode.FORCE_CARVE,
recovered.restoredDecision().terrain().resolvedMode());
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
assertTrue(recovered.referenceMinChunkX()
< NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4);
}
@Test
public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() {
long seed = 22817L;
ChunkPos origin = new ChunkPos(-3, 6);
OceanMonumentStructure structure = structure();
StructureStart natural = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, natural,
plan, natural, true, NativeStructureOwnershipRecord::capture);
assertNull(recovered);
}
@Test
public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() {
long seed = 991723L;
ChunkPos origin = new ChunkPos(2, 4);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart changedGeometry = monumentStart(structure, origin, seed);
for (StructurePiece piece : changedGeometry.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureStartPlan originalIdentity = plan(
origin, "original-identity", 0);
NativeStructureStartPlan changedIdentity = plan(
new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0);
assertNotEquals(
StructurePlacementGrid.placementIdentity(originalIdentity.placement()),
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
changedIdentity, expected, false, NativeStructureOwnershipRecord::capture));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, changedGeometry,
plan(origin, "geometry-check", 0), expected, false, NativeStructureOwnershipRecord::capture));
}
@Test
public void recoveredEnvelopeStillRequiresExactFingerprint() {
long seed = 81337L;
ChunkPos origin = new ChunkPos(-8, -9);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
plan(origin, "fingerprint-check", 16), expected, false, NativeStructureOwnershipRecord::capture);
assertNotNull(recovered);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(0, 0, 1);
}
assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved));
}
@Test
public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() {
String structureKey = "minecraft:monument";
long seed = 648231L;
ChunkPos origin = new ChunkPos(5, -6);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-envelope-refresh", StructureTerrainMode.VACUUM, 0);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, content), plan);
NativeStructureOwnershipRecord refreshed =
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, stale);
assertNotNull(refreshed);
assertNotEquals(stale, refreshed);
assertEquals(stale.schema(), refreshed.schema());
assertEquals(stale.ownershipKey(), refreshed.ownershipKey());
assertEquals(stale.placementIdentity(), refreshed.placementIdentity());
assertEquals(stale.baseY(), refreshed.baseY());
assertEquals(stale.locatorY(), refreshed.locatorY());
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
assertEquals(stale.decision(), refreshed.decision());
assertEquals(StructureTerrainMode.VACUUM,
refreshed.restoredDecision().terrain().resolvedMode());
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX());
assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX());
assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ());
assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ());
assertTrue(hasExpandedCoverage(stale, refreshed));
assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, refreshed));
}
@Test
public void staleEnvelopeCannotRefreshAgainstDifferentContent() {
String structureKey = "minecraft:monument";
long seed = 412987L;
ChunkPos origin = new ChunkPos(-2, 7);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-content-check", StructureTerrainMode.VACUUM, 0);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, expected, NativeStructureReferenceEnvelope.contentBounds(expected)), plan);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, moved, stale));
}
@Test
public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() {
String structureKey = "minecraft:monument";
long seed = 927451L;
ChunkPos origin = new ChunkPos(3, 8);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, envelope), plan);
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
@Test
public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() {
String structureKey = "minecraft:monument";
long seed = 381729L;
ChunkPos origin = new ChunkPos(0, 0);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
int maximumReferenceBlockX = ((origin.x()
+ StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
int shiftX = maximumReferenceBlockX - initial.maxX();
for (StructurePiece piece : start.getPieces()) {
piece.move(shiftX, 0, 0);
}
NativeStructureStartPlan plan = plan(
origin, "clipped-vacuum", StructureTerrainMode.VACUUM, 0);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord.capture(
NativeStructureOwnershipFingerprint.capture(
structureKey, start, envelope), plan);
assertEquals(origin.x() + StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS,
ownership.referenceMaxChunkX());
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
private static OceanMonumentStructure structure() {
return new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building =
new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(building)));
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
int horizontalPadding) {
return plan(origin, placementId,
StructureTerrainMode.FORCE_CARVE, horizontalPadding);
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
StructureTerrainMode terrainMode,
int horizontalPadding) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(terrainMode)
.setHorizontalPadding(horizontalPadding);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId(placementId)
.setTerrain(terrain)
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
NativeStructureReferenceEnvelope.contentBounds(
monumentStart(structure(), origin, 1L)).minY()
);
}
private static boolean hasExpandedCoverage(
NativeStructureOwnershipRecord stale,
NativeStructureOwnershipRecord refreshed) {
for (int chunkX = refreshed.referenceMinChunkX();
chunkX <= refreshed.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = refreshed.referenceMinChunkZ();
chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) {
if (!stale.covers(chunkX, chunkZ)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,542 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.terrain.structure.StructurePalette;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import art.arcane.iris.structure.placement.IrisStructureYBand;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorEncaseTest {
private static final long TEST_SEED = 1234L;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(1)
.setCeilingPadding(1)
.setFloorPadding(1),
null);
assertEquals(Blocks.DIRT.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
}
@Test
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
StructureStart start = start(TerrainAdjustment.BURY, -1);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(StructureTerrainMode.ENCASE),
null);
assertEquals(-1, bounds.minY());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
state(blocks, bounds.minX(), -1, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), 0, bounds.minZ()));
}
@Test
public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() {
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64));
assertEquals(Blocks.STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64));
}
@Test
public void sourceEncaseMaterialFollowsNearbyIrisTerrain() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, 64, 8);
put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, -20, 8);
put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState());
put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
NativeStructureTerrainIntegrator.captureSourceTerrain(
world(new HashMap<>()), area,
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
"test:bounded-snapshot", start,
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE))));
assertNotNull(sourceTerrain);
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
}
@Test
public void customDimensionKeyUsesRegisteredVanillaDimensionType() {
ResourceKey<Level> customDimension = ResourceKey.create(
Registries.DIMENSION, Identifier.parse("iris:custom_nether"));
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.NETHER, null, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.END, null, 64));
}
@Test
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(StructureTerrainMode.ENCASE)
.setEncasePalette(palette),
NativeStructurePostProcessorEncaseTest::tuffBlock);
assertEquals(Blocks.TUFF.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
}
@Test
public void omittedTerrainConfigurationRetainsSourceSemantics() {
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
StructureTerrainSettings resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(StructureTerrainMode.SOURCE, resolved.resolvedMode());
}
}
@Test
public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
int groundY = bounds.minY();
BoundingBox area = new BoundingBox(
bounds.minX() - 7, groundY - 13, bounds.minZ(),
bounds.maxX(), groundY + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:bury", start,
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 6, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 7, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 13, bounds.minZ()));
}
@Test
public void sourceEncapsulateWrapsTheFullPieceBounds() {
StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 13, bounds.minY(), bounds.minZ(),
bounds.maxX(), bounds.maxY() + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:encapsulate", start,
new IrisStructureTerrain().setMode(StructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ()));
}
@Test
public void sourceTerrainDensityOnlyUsesRigidPoolPieces() {
PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING);
PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID);
assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching));
assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid));
}
@Test
public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() {
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0));
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0));
}
@Test
public void explicitTerrainConfigurationWinsOverAutoEncase() {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(StructureTerrainMode.PRESERVE);
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
}
@Test
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
IrisStructureTerrain source = new IrisStructureTerrain()
.setMode(StructureTerrainMode.SOURCE);
IrisStructureTerrain preserve = new IrisStructureTerrain()
.setMode(StructureTerrainMode.PRESERVE);
IrisStructureTerrain encase = new IrisStructureTerrain()
.setMode(StructureTerrainMode.ENCASE);
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BEARD_BOX, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), encase));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), source));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), preserve));
}
@Test
public void encaseReservesTheNeighborChunkEnvelope() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox sourceBounds = generated.getBoundingBox();
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(4);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
generated.getStructure(),
0,
terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(sourceBounds, wrapped.getBoundingBox());
assertEquals(content.minX() - 4, referenceBounds.minX());
assertEquals(content.maxX() + 4, referenceBounds.maxX());
assertEquals(content.minZ() - 4, referenceBounds.minZ());
assertEquals(content.maxZ() + 4, referenceBounds.maxZ());
assertEquals(generated.getPieces().size(), wrapped.getPieces().size());
}
@Test
public void referenceEnvelopeRejectsChunkCoordinateOverflow() {
StructureStart generated = start(TerrainAdjustment.NONE, 64);
StructureStart extreme = new StructureStart(
generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0,
new PiecesContainer(generated.getPieces()));
assertThrows(IllegalStateException.class, () ->
NativeStructureReferenceEnvelope.referenceBounds(
extreme, extreme.getStructure(), new IrisStructureTerrain()));
}
@Test
public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(StructureTerrainMode.ENCASE)
.setHorizontalPadding(112);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated, generated.getStructure(), 0, terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(content.minX() - 112, referenceBounds.minX());
assertEquals(content.maxX() + 112, referenceBounds.maxX());
}
@Test
public void yBandRelocatesTheStartMidpointDeterministically() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart first = start(TerrainAdjustment.BURY, 64);
StructureStart second = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
NativeStructureVerticalPlacer.applyVerticalShift(
second, -64, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = first.getBoundingBox();
assertEquals(bounds.minY(), second.getBoundingBox().minY());
assertTrue(bounds.minY() >= -120);
assertTrue(bounds.maxY() <= -20);
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(0, repeated);
}
@Test
public void yBandClampsWhenTheBandCannotContainTheStructure() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
StructureStart start = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = start.getBoundingBox();
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
assertTrue(midpoint >= -50);
assertTrue(midpoint <= -45);
}
@Test
public void yBandReplacesBothTheBlindShiftAndBurial() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getBoundingBox();
int height = bounds.maxY() - bounds.minY();
NativeStructureVerticalPlacer.applyVerticalShift(
start, -200, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
}
@Test
public void preserveSourceYWinsOverTheConfiguredBand() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart start = start(TerrainAdjustment.BURY, 64);
int minY = start.getBoundingBox().minY();
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, true, band, (x, z) -> 40);
assertEquals(0, offset);
assertEquals(minY, start.getBoundingBox().minY());
}
private static BlockState tuffBlock(StructurePalette palette, RNG rng, int x, int y, int z) {
assertNotNull(palette);
assertNotNull(rng);
return Blocks.TUFF.defaultBlockState();
}
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) {
BlockPos position = new BlockPos(0, 64, 0);
StructurePoolElement element = StructurePoolElement.single(
"minecraft:empty",
Holder.direct(new StructureProcessorList(List.of(
BlockIgnoreProcessor.STRUCTURE_AND_AIR))),
LiquidSettings.APPLY_WATERLOGGING
).apply(projection);
return new PoolElementStructurePiece(
null,
element,
position,
0,
Rotation.NONE,
new BoundingBox(position),
LiquidSettings.APPLY_WATERLOGGING);
}
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
return state(blocks, position.getX(), position.getY(), position.getZ());
}
if (methodName.equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
BlockState blockState = (BlockState) arguments[1];
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
return true;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "encase-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot(
WorldGenLevel world, BoundingBox area) {
BitSet requiredLayers = new BitSet(area.getYSpan());
requiredLayers.set(0, area.getYSpan());
return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture(
world, area, requiredLayers);
}
private static void put(Map<BlockPos, BlockState> blocks,
int x, int y, int z, BlockState state) {
blocks.put(new BlockPos(x, y, z), state);
}
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
}
}
@@ -0,0 +1,206 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.volmlib.util.structure.StructureTerrainMode;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorMonumentTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(39, start.getBoundingBox().minY());
assertEquals(61, start.getBoundingBox().maxY());
}
@Test
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(26, building.getBoundingBox().minY());
assertEquals(48, building.getBoundingBox().maxY());
assertEquals(26, start.getBoundingBox().minY());
assertEquals(48, start.getBoundingBox().maxY());
assertTrue(children.size() > 20);
for (int i = 0; i < children.size(); i++) {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@Test
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-10, offset);
assertEquals(29, start.getBoundingBox().minY());
assertEquals(51, start.getBoundingBox().maxY());
}
@Test
public void referenceEnvelopeDoesNotAddMonumentPieces() {
StructureStart start = monumentStart(1337L);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start,
start.getStructure(),
0,
new IrisStructureTerrain()
.setMode(StructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(4));
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
wrapped, "minecraft:monument", 0, 63, -64, 320,
false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512,
false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
ExecutorService executor = Executors.newFixedThreadPool(8);
CountDownLatch release = new CountDownLatch(1);
List<Future<?>> futures = new ArrayList<>();
try {
for (int task = 0; task < 16; task++) {
futures.add(executor.submit(() -> {
release.await();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
reloaded, "minecraft:monument", 0, 50, -256, 512);
return null;
}));
}
release.countDown();
for (Future<?> future : futures) {
future.get();
}
} finally {
executor.shutdownNow();
}
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
return;
}
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
}
private static StructureStart monumentStart(long seed) {
ChunkPos chunkPos = new ChunkPos(0, 0);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
RandomSource.create(seed), -29, -29, Direction.NORTH);
return new StructureStart(
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
}
private static OceanMonumentStructure monumentStructure() {
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
}
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
}
private static int[] minimumYs(List<StructurePiece> pieces) {
int[] ys = new int[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
ys[i] = pieces.get(i).getBoundingBox().minY();
}
return ys;
}
}
@@ -0,0 +1,204 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureSurfaceFitter;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReflection;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorScatteredFeatureTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
StructureStart start = desertStart(piece);
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
assertEquals(1, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(65, piece.getBoundingBox().minY());
assertEquals(65, start.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment(-2));
assertEquals(65, piece.getBoundingBox().minY());
}
@Test
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
StructureStart start = jungleStart(piece);
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
assertEquals(11, offset);
assertEquals(75, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(75, piece.getBoundingBox().minY());
}
@Test
public void repeatedAlignmentIsIdempotent() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
assertEquals(17, initialOffset);
assertEquals(0, repeatedOffset);
assertEquals(81, piece.getBoundingBox().minY());
}
@Test
public void scatteredAlignmentStaysInsideWorldBounds() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
StructureStart start = desertStart(piece);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
assertEquals(241, offset);
assertEquals(305, piece.getBoundingBox().minY());
assertEquals(319, start.getBoundingBox().maxY());
}
@Test
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
StructureStart desertStart = desertStart(desert);
StructureStart jungleStart = jungleStart(jungle);
StructureStart swampStart = swampStart(swamp);
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
assertEquals(0, terrainQueries.get());
assertEquals(64, swamp.getBoundingBox().minY());
}
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
assertEquals(int.class, field.getType());
assertTrue(Modifier.isProtected(field.getModifiers()));
assertFalse(Modifier.isFinal(field.getModifiers()));
assertFalse(Modifier.isStatic(field.getModifiers()));
assertTrue(field.trySetAccessible());
}
@Test
public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() {
StructureStart pyramidOverride = swampStart(
new SwampHutPiece(RandomSource.create(43L), 0, 0));
int pyramidMinY = pyramidOverride.getBoundingBox().minY();
int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
pyramidOverride, "minecraft:desert_pyramid", 5,
63, -64, 320, false, false, null, (x, z) -> 200);
assertEquals(5, pyramidOffset);
assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY());
StructureStart monumentOverride = swampStart(
new SwampHutPiece(RandomSource.create(47L), 0, 0));
int monumentMinY = monumentOverride.getBoundingBox().minY();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
monumentOverride, "minecraft:monument", 0, 40, -64, 320);
assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY());
}
private static StructureStart desertStart(StructurePiece piece) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart jungleStart(StructurePiece piece) {
Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart swampStart(StructurePiece piece) {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart start(Structure structure, StructurePiece piece) {
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static final class TestDesertPyramidPiece extends DesertPyramidPiece {
private TestDesertPyramidPiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment(int offset) {
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset);
}
}
private static final class TestJungleTemplePiece extends JungleTemplePiece {
private TestJungleTemplePiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment() {
return updateAverageGroundHeight((LevelAccessor) null, null, 0);
}
}
}
@@ -0,0 +1,388 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureSurfaceFitter;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVegetationClearer;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
private static final long TEST_SEED = 1234L;
private static final int MIN_Y = -64;
private static final int MAX_Y = 319;
private static final int GROUND_Y = 64;
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
fillLayer(chunk, y, leaves());
}
write(chunk, 4, bounds.minY() - 1, 4, log());
clearVegetation(chunk, start);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
}
}
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
}
@Test
public void buriedStructuresPreserveTheForestAboveThem() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, -20);
write(chunk, 8, GROUND_Y + 1, 8, log());
write(chunk, 8, GROUND_Y + 2, 8, log());
write(chunk, 8, GROUND_Y + 3, 8, log());
write(chunk, 8, GROUND_Y + 4, 8, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
}
@Test
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
int inside = bounds.minY() + 4;
write(chunk, 0, inside, 0, log());
write(chunk, 0, GROUND_Y + 40, 0, leaves());
write(chunk, 12, inside, 12, log());
write(chunk, 12, GROUND_Y + 40, 12, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
}
@Test
public void clearingStopsAtThePieceEnvelopeBoundaries() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
write(chunk, 4, bounds.maxY() + 1, 4, log());
write(chunk, 4, bounds.maxY(), 4, log());
write(chunk, 4, bounds.minY() - 1, 4, log());
write(chunk, 4, bounds.minY() - 2, 4, log());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
}
@Test
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
ProtoChunk chunk = chunk();
StructureStart low = desertStart(0, 0, GROUND_Y);
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
int between = GROUND_Y + 20;
write(chunk, 4, between, 4, log());
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(low, high));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
}
@Test
public void validStartsReachIntersectionProcessing() {
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
desertStart(0, 0, GROUND_Y + 15)));
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
StructureStart.INVALID_START));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@Test
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
@Test
public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() {
ProtoChunk chunk = chunk();
for (int y = 80; y <= 102; y++) {
write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState());
}
for (int y = 103; y <= 105; y++) {
write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState());
}
write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y);
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4)));
for (int y = 94; y <= 106; y++) {
assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
}
}
@Test
public void loweringSurfacePreservesCactusOnUnchangedColumns() {
ProtoChunk chunk = chunk();
for (int x : List.of(4, 5)) {
for (int y = 60; y <= 64; y++) {
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
}
write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
}
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir());
assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir());
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
}
@Test
public void loweringSurfaceKeepsTreeBoundaries() {
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) {
ProtoChunk chunk = chunk();
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
write(chunk, 4, 65, 4, boundary);
write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4)));
}
}
@Test
public void loweringSurfaceRemovesUnsupportedPlankDecoration() {
ProtoChunk chunk = chunk();
for (int x : List.of(4, 5)) {
for (int y = 60; y <= 64; y++) {
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
}
write(chunk, x, 65, 4, Blocks.OAK_PLANKS.defaultBlockState());
write(chunk, x, 66, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, x, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
}
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 60, 4)));
for (int y = 61; y <= 67; y++) {
assertTrue(chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
}
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
assertEquals(Blocks.OAK_PLANKS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 67, 4)));
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
}
private static StructureStart desertStart(int originX, int originZ, int topY) {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(
HolderSet.empty(), Map.of(),
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static ProtoChunk chunk() {
return new ProtoChunk(
new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
}
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(chunk, x, y, z, state);
}
}
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static BlockState log() {
return Blocks.OAK_LOG.defaultBlockState();
}
private static BlockState leaves() {
return Blocks.OAK_LEAVES.defaultBlockState();
}
private static BlockState air() {
return Blocks.AIR.defaultBlockState();
}
private static WorldGenLevel world(ChunkAccess chunk) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
return chunk.getBlockState((BlockPos) arguments[0]);
}
if (methodName.equals("setBlock")) {
return chunk.setBlockState(
((BlockPos) arguments[0]).immutable(),
(BlockState) arguments[1], (int) arguments[2]) != null;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "vegetation-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
}
@@ -0,0 +1,240 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTemplateOccupancy;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReflection;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.util.valueproviders.ConstantInt;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureTemplateOccupancyTest {
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
private static final int TEMPLATE_WIDTH = 32;
private static final int TERRAIN_HEIGHT = 64;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
// 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla
// runs processors across the whole piece. Iris mirrors the pinned version's semantics.
assertEquals(expectedClippedColumns(), recorder.heightColumns);
}
@Test
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(List.of(
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder);
assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns);
}
@Test
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception {
Method contract = clipContractMethod();
Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null);
for (StructureProcessor processor : List.of(
BlockIgnoreProcessor.STRUCTURE_BLOCK,
JigsawReplacementProcessor.INSTANCE,
LavaSubmergedBlockProcessor.INSTANCE,
NopProcessor.INSTANCE,
BlackstoneReplaceProcessor.INSTANCE,
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new RuleProcessor(List.of()),
new BlockAgeProcessor(0.5F),
protectedBlockProcessor())) {
assertFalse(processor.getClass().getName(),
(Boolean) contract.invoke(processor));
}
assertTrue((Boolean) contract.invoke(
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))));
}
@Test
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
PoolElementStructurePiece piece = straddlingPiece(List.of());
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
}
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
NativeStructureTemplateOccupancy.resolve(
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
PROCESSING_AREA::isInside, amount -> {
});
}
private static PoolElementStructurePiece straddlingPiece(
List<StructureProcessor> processors) throws Exception {
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
return new PoolElementStructurePiece(
null, new InlineSinglePoolElement(template(), processors),
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
}
private static Set<Integer> columns(int minimumX, int maximumX) {
Set<Integer> columns = new TreeSet<>();
for (int x = minimumX; x <= maximumX; x++) {
columns.add(x);
}
return columns;
}
private static Method clipContractMethod() {
try {
return StructureProcessor.class.getMethod("evaluatesEntirePieceState");
} catch (NoSuchMethodException absent) {
return null;
}
}
private static Set<Integer> expectedClippedColumns() {
return clipContractMethod() != null
? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX())
: columns(0, TEMPLATE_WIDTH - 1);
}
private static StructureProcessor protectedBlockProcessor() throws Exception {
// Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2.
Constructor<?> constructor = ProtectedBlockProcessor.class.getConstructors()[0];
Class<?> parameter = constructor.getParameterTypes()[0];
Object argument = parameter == HolderSet.class
? HolderSet.empty()
: TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air"));
return (StructureProcessor) constructor.newInstance(argument);
}
private static StructureTemplateManager forbiddenTemplateManager() {
throw new AssertionError("Inline templates must not resolve a template manager");
}
private static StructureTemplate template() throws Exception {
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
blocks.add(new StructureTemplate.StructureBlockInfo(
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
}
Constructor<StructureTemplate.Palette> constructor =
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
assertTrue(constructor.trySetAccessible());
StructureTemplate template = new StructureTemplate();
template.palettes.add(constructor.newInstance(blocks));
return template;
}
private static WorldGenLevel world(ColumnRecorder recorder) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getHeight") && arguments.length == 3) {
recorder.heightColumns.add((Integer) arguments[1]);
return TERRAIN_HEIGHT;
}
if (methodName.equals("getBlockState")) {
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
return Blocks.STONE.defaultBlockState();
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "occupancy-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, handler);
}
private static final class ColumnRecorder {
private final Set<Integer> heightColumns = new TreeSet<>();
private final Set<Integer> stateColumns = new TreeSet<>();
}
private static final class InlineSinglePoolElement extends SinglePoolElement {
private InlineSinglePoolElement(
StructureTemplate template, List<StructureProcessor> processors) {
super(Either.right(template),
Holder.direct(new StructureProcessorList(processors)),
StructureTemplatePool.Projection.RIGID,
Optional.<LiquidSettings>empty());
}
}
}
@@ -0,0 +1,131 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainHeightCache;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.OptionalInt;
import java.util.function.IntSupplier;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class NativeTerrainHeightCacheTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
AtomicInteger calls = new AtomicInteger();
NativeTerrainHeightCache.Query[] queries = {
query(1, -1, 1),
query(1, 1, -1),
query(2, -1, 1),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
};
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
assertEquals(queries.length, calls.get());
cache.evictRuntime(1);
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
}
@Test
public void allIntegerResultsAreCacheable() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
}
@Test
public void failedResolutionCanBeRetried() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
throw new IllegalStateException("terrain unavailable");
}));
assertEquals(42, cached(cache, query, () -> 42));
}
@Test
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
entered.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interruption);
}
return 9;
}));
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
release.countDown();
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void boundedCacheKeepsRecentlyReadCoordinates() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
for (int x = 0; x < 65_536; x++) {
cached(cache, query(1, x, 0), () -> 8);
}
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
cached(cache, query(1, 65_536, 0), () -> 9);
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
}
@Test
public void unavailableResolvedHeightIsNotCached() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
}
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
IntSupplier resolver) {
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
}
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.world.history.TerrainNativeBlockKeys;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTransitionColumn;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public final class NativeTransitionColumnTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
String key = "itemsadder:rocks/ruby_ore";
NativeBlockState custom = mock(NativeBlockState.class);
NativeBlockState carrier = mock(NativeBlockState.class);
when(custom.placementBaseState()).thenReturn(carrier);
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockOrNull(key)).thenReturn(custom);
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
TerrainBoundarySignature signature = signature(key);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
platforms.when(IrisPlatforms::isBound).thenReturn(true);
platforms.when(IrisPlatforms::get).thenReturn(platform);
NoiseColumn column = NativeTransitionColumn.column(signature.geometry(), height, TerrainNativeBlockKeys::placementKey);
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertEquals(-14, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.OCEAN_FLOOR_WG, height, TerrainNativeBlockKeys::placementKey));
}
}
@Test
public void preservesNativePropertiesCavesAndFluids() {
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]").geometry(),
LevelHeightAccessor.create(-16, 32), TerrainNativeBlockKeys::placementKey);
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
assertTrue(column.getBlock(0).isAir());
}
@Test
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
assertEquals(-12, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.WORLD_SURFACE_WG, height, TerrainNativeBlockKeys::placementKey));
assertEquals(-14, NativeTransitionColumn.height(signature.geometry(), Heightmap.Types.OCEAN_FLOOR_WG, height, TerrainNativeBlockKeys::placementKey));
}
@Test
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(signature("missing:removed_block").geometry(), LevelHeightAccessor.create(-16, 32), TerrainNativeBlockKeys::placementKey));
}
private static TerrainBoundarySignature signature(String solidState) {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
"minecraft:water[level=3]", false)));
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
geometry);
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagLoader;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class NativeBlockTags {
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
private NativeBlockTags() {
}
public static void bindHeightmapFixtures() {
originalTags = new HashMap<>();
for (HolderSet.Named<Block> tag : BuiltInRegistries.BLOCK.getTags().toList()) {
originalTags.put(tag.key(), tag.stream().toList());
}
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder(),
Blocks.OAK_LEAVES.builtInRegistryHolder(), Blocks.SPRUCE_LEAVES.builtInRegistryHolder());
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, Map.of(
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
}
public static void restoreTags() {
if (originalTags == null) {
return;
}
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, originalTags)).apply();
originalTags = null;
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.resources.RegistryOps;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.attribute.modifier.MobSpawnSettingsModifier;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CustomBiomeCodecTest {
private static HolderLookup.Provider registries;
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = VanillaRegistries.createWorldLookup();
}
@Test
public void nativeCodecAcceptsCustomColorsAndSpawnCounts() {
IrisBiomeCustom custom = new IrisBiomeCustom();
custom.setId("codec_test");
custom.setWaterColor("#102030");
custom.setFogColor("#405060");
custom.setSpawnRarity(2);
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn();
spawn.setType("minecraft:zombie");
spawn.setGroup(IrisBiomeCustomSpawnType.MONSTER);
spawn.setMinCount(2);
spawn.setMaxCount(4);
spawn.setWeight(7);
custom.setSpawns(new KList<>(spawn));
Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, registries),
JsonParser.parseString(custom.generateJson(new DataFixerV263()))).getOrThrow();
MobSpawnSettings settings = biome.getAttributes().applyModifier(EnvironmentAttributes.NATURAL_MOB_SPAWNS,
EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue());
MobSpawnSettings.SpawnerData decoded = settings.getMobsToSpawn(MobCategory.MONSTER).unwrap().getFirst().value();
assertEquals(2, decoded.count().minInclusive());
assertEquals(4, decoded.count().maxInclusive());
assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF);
assertEquals(MobSpawnSettingsModifier.overlay(),
biome.getAttributes().get(EnvironmentAttributes.NATURAL_MOB_SPAWNS).modifier());
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Proxy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
IrisDimension host = new IrisDimension();
host.setLoadKey("Host");
IrisDimension upper = new IrisDimension();
upper.setLoadKey("Layers/Upper");
assertEquals("layers:upper/aurora", BukkitBiomePolicy.customBiomeKey(upper, "Aurora"));
assertNotEquals(
BukkitBiomePolicy.customBiomeKey(host, "Aurora"),
BukkitBiomePolicy.customBiomeKey(upper, "Aurora")
);
}
@Test
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
assertFalse(BukkitBiomePolicy.isBiomeCacheable(temporary, 12, -8));
assertTrue(BukkitBiomePolicy.isBiomeCacheable(stable, 12, -8));
}
private static Engine engineAnsweringNaturalFallback(
boolean naturalFallback,
int expectedX,
int expectedZ
) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (!method.getName().equals("answersFromNaturalTerrain")) {
throw new UnsupportedOperationException(method.getName());
}
assertEquals(Integer.valueOf(expectedX), arguments[0]);
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
return naturalFallback;
}
);
}
}
@@ -0,0 +1,97 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import org.junit.Test;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
Semaphore admission = new Semaphore(1, true);
admission.acquire();
CountDownLatch exclusiveEntered = new CountDownLatch(1);
CountDownLatch releaseExclusive = new CountDownLatch(1);
AtomicBoolean dependentFinished = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
boolean stageReleased = false;
try {
Future<?> exclusive = executor.submit(() -> {
boolean acquired = false;
try {
admission.acquire();
acquired = true;
exclusiveEntered.countDown();
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} finally {
if (acquired) {
admission.release();
}
}
});
awaitQueueLength(admission, 1);
CompletableFuture<Void> outward = new CompletableFuture<>();
outward.thenRun(() -> {
try {
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
dependentFinished.set(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
});
admission.release();
stageReleased = true;
assertTrue(outward.complete(null));
assertTrue(dependentFinished.get());
releaseExclusive.countDown();
exclusive.get(2, TimeUnit.SECONDS);
assertEquals(1, admission.availablePermits());
} finally {
if (!stageReleased) {
admission.release();
}
releaseExclusive.countDown();
executor.shutdownNow();
}
}
@Test
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() {
CompletableFuture<Void> delegate = new CompletableFuture<>();
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
assertTrue(delegate.cancel(false));
assertFalse(transformed.isCancelled());
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
assertTrue(failure.getCause() instanceof CancellationException);
}
private static void awaitQueueLength(Semaphore semaphore, int expected) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertTrue("Expected at least " + expected + " queued semaphore threads",
semaphore.getQueueLength() >= expected);
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
@Test
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
AtomicInteger irisReferences = new AtomicInteger();
AtomicInteger nativeReferences = new AtomicInteger();
Pair<BlockPos, Holder<Structure>> irisSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeFar, () -> nativeReferences.incrementAndGet());
assertSame(irisNear, irisSelected);
assertEquals(1, irisReferences.get());
assertEquals(0, nativeReferences.get());
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeNear, () -> nativeReferences.incrementAndGet());
assertSame(nativeNear, nativeSelected);
assertEquals(1, irisReferences.get());
assertEquals(1, nativeReferences.get());
}
}
@@ -0,0 +1,153 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.platform.bukkit.nms.BukkitGeneratorContext;
import org.bukkit.World;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeSourceImpl;
import net.minecraft.server.level.ServerLevel;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.GenerationSessionManager;
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.generation.context.IrisContext;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisChunkGeneratorSpawnAdmissionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
Fixture fixture = new Fixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService reloader = Executors.newSingleThreadExecutor();
AtomicInteger snapshots = new AtomicInteger();
long previousSession = fixture.sessions.currentSessionId();
try {
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(previousSession, fixture.readSession.get());
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
Future<?> query = owner.submit(() -> {
assertTrue(fixture.query().isEmpty());
assertNull(IrisContext.get());
});
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
query.get(2L, TimeUnit.SECONDS);
assertEquals(1, fixture.savedReads.get());
assertEquals(1, fixture.routes.get());
fixture.sessions.activateNextSession();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
}, reloader);
cutover.get(3L, TimeUnit.SECONDS);
assertEquals(1, snapshots.get());
assertEquals(0, fixture.sessions.activeLeases());
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
assertTrue(fixture.readSession.get() > previousSession);
assertEquals(2, fixture.savedReads.get());
assertEquals(2, fixture.routes.get());
assertEquals(0, fixture.sessions.activeLeases());
} finally {
owner.shutdownNow();
reloader.shutdownNow();
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
}
}
private static void setField(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
Field field = NativeChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class Fixture {
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final IrisEngine engine = mock(IrisEngine.class);
private final NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
private final StructureManager structures = mock(StructureManager.class);
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
private final ServerLevel level = mock(ServerLevel.class);
private final AtomicInteger savedReads = new AtomicInteger();
private final AtomicInteger routes = new AtomicInteger();
private final AtomicLong readSession = new AtomicLong();
private Fixture() throws Exception {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
ChunkGenerator delegate = mock(ChunkGenerator.class);
NativeBiomeSourceImpl source = mock(NativeBiomeSourceImpl.class);
when(level.getBiome(new BlockPos(0, 64, 0))).thenReturn(biome);
setField(generator, "runtimeLevel", level);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
assertAdmitted();
savedReads.incrementAndGet();
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
});
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
assertAdmitted();
routes.incrementAndGet();
return null;
});
when(delegate.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
.thenReturn(expected);
setField(generator, "context", new BukkitGeneratorContext(new BukkitGeneratorContext.Options(1L, engine, mock(World.class))));
setField(generator, "delegate", delegate);
setField(generator, "customBiomeSource", source);
}
private WeightedList<MobSpawnSettings.SpawnerData> query() {
return generator.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
}
private void assertAdmitted() {
assertEquals(1, sessions.activeLeases());
assertSame(engine, IrisContext.get().getEngine());
readSession.set(IrisContext.get().getGenerationSessionId());
}
}
}
@@ -0,0 +1,439 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureSetFilter;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
import art.arcane.volmlib.util.collection.KList;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import com.mojang.serialization.MapCodec;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import org.junit.Test;
import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
public class NMSBindingDatapackStructureScopeTest {
private static final String SOURCE = "https://example.test/managed.zip";
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:pillager_outpost");
Holder<StructureSet> managedSet = structureSetHolder(
"managed:illager_barracks", vanillaStructure);
DatapackStructureScopeIndex index = index(
List.of(),
List.of("managed:illager_barracks"));
NativeStructureSetFilter.Selection vanilla = NativeStructureSetFilter.filter(
List.of(managedSet), index, Set.of());
NativeStructureSetFilter.Selection declaring = NativeStructureSetFilter.filter(
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(0, vanilla.structureSets().size());
assertEquals(1, vanilla.excludedManagedSets());
assertEquals(1, declaring.structureSets().size());
assertSame(managedSet, declaring.structureSets().getFirst());
}
@Test
public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:village_plains");
Holder<Structure> managedStructure = structureHolder("managed:tavern");
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:villages", vanillaStructure, managedStructure);
DatapackStructureScopeIndex index = index(
List.of("managed:tavern"),
List.of());
NativeStructureSetFilter.Selection vanilla = NativeStructureSetFilter.filter(
List.of(vanillaSet), index, Set.of());
NativeStructureSetFilter.Selection declaring = NativeStructureSetFilter.filter(
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(1, vanilla.structureSets().size());
assertEquals(1, vanilla.structureSets().getFirst().value().structures().size());
assertSame(vanillaStructure,
vanilla.structureSets().getFirst().value().structures().getFirst().structure());
assertSame(vanillaSet.value().placement(),
vanilla.structureSets().getFirst().value().placement());
assertSame(vanillaSet, declaring.structureSets().getFirst());
}
@Test
public void setWithNoAllowedDefinitionsIsRemoved() {
Holder<StructureSet> unmanagedSet = structureSetHolder(
"minecraft:custom", structureHolder("managed:only"));
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(unmanagedSet), index, Set.of());
assertEquals(0, selection.structureSets().size());
}
@Test
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
ResourceKey<StructureSet> key = ResourceKey.create(
Registries.STRUCTURE_SET,
Identifier.parse("managed:illager_barracks"));
List<StructureSet.StructureSelectionEntry> entries = List.of(
new StructureSet.StructureSelectionEntry(
structureHolder("minecraft:pillager_outpost"), 1));
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 10387312)));
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 14357620)));
DatapackStructureScopeIndex scopeIndex = index(
List.of(), List.of("managed:illager_barracks"));
NativeStructureSetFilter.StructureSetKeyIndex keyIndex =
NativeStructureSetFilter.keyIndex(List.of(registered));
NativeStructureSetFilter.Selection excluded =
NativeStructureSetFilter.filter(
List.of(spigotDirect),
scopeIndex,
Set.of(),
new IrisImportedStructureControl(),
keyIndex);
NativeStructureSetFilter.Selection retained =
NativeStructureSetFilter.filter(
List.of(spigotDirect),
scopeIndex,
scopeIndex.declaredSources(List.of(SOURCE)),
new IrisImportedStructureControl(),
keyIndex);
assertEquals(0, excluded.structureSets().size());
assertEquals(1, excluded.excludedManagedSets());
assertSame(spigotDirect, retained.structureSets().getFirst());
assertEquals(1, retained.retainedManagedSets());
}
@Test
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
InputStream classResource = NativeStructureSetFilter.class
.getResourceAsStream("NativeStructureSetFilter.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
}
}
@Test
public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() {
Holder<StructureSet> complexes = structureSetHolder(
"minecraft:nether_complexes",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("minecraft:fortress"));
Holder<StructureSet> fossils = structureSetHolder(
"minecraft:nether_fossils",
new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921),
structureHolder("minecraft:nether_fossil"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
RandomSpreadStructurePlacement scaledComplexes =
(RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement();
RandomSpreadStructurePlacement unchangedFossils =
(RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement();
assertEquals(26, scaledComplexes.spacing());
assertEquals(4, scaledComplexes.separation());
assertEquals(2, unchangedFossils.spacing());
assertSame(fossils, selection.structureSets().get(1));
assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() {
Holder<StructureSet> exclusionTarget = structureSetHolder(
"example:target", structureHolder("example:target"));
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new UnsupportedPlacement(Optional.of(
new AbstractSpreadingStructurePlacement.ExclusionZone(exclusionTarget, 1))),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
assertSame(custom, selection.structureSets().getFirst());
}
@Test
public void frequencyOnlyPassRebindsDependentExclusionTarget() {
Holder<StructureSet> target = structureSetHolder(
"example:target",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("example:target"));
RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(target, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> dependent = structureSetHolder(
"example:dependent",
dependentPlacement,
structureHolder("example:dependent"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:target")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
assertNotSame(dependent, scaledDependent);
assertNotSame(target, scaledTarget);
assertSame(scaledTarget, NativeStructureSetFilter.exclusionZone(
scaledDependent.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledTarget.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassRebindsAffectedExclusionCycle() {
MutableStructureSetHolder first = new MutableStructureSetHolder("example:first");
MutableStructureSetHolder second = new MutableStructureSetHolder("example:second");
first.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
101,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(second, 1)),
32,
8,
RandomSpreadType.LINEAR),
"example:first"));
second.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
102,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(first, 1)),
27,
4,
RandomSpreadType.LINEAR),
"example:second"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:second")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
assertSame(scaledSecond, NativeStructureSetFilter.exclusionZone(
scaledFirst.value().placement()).orElseThrow().otherSet());
assertSame(scaledFirst, NativeStructureSetFilter.exclusionZone(
scaledSecond.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledSecond.value().placement()).spacing());
}
@Test
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new CustomRandomSpreadPlacement(),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:custom")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
}
@Test
public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() {
Holder<StructureSet> managedSet = structureSetHolder(
"managed:blocked",
structureHolder("managed:blocked"));
RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(managedSet, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:allowed",
originalPlacement,
structureHolder("minecraft:village_plains"));
DatapackStructureScopeIndex index = index(
List.of("managed:blocked"),
List.of("managed:blocked"));
NativeStructureSetFilter.Selection selection = NativeStructureSetFilter.filter(
List.of(vanillaSet, managedSet), index, Set.of());
assertEquals(1, selection.structureSets().size());
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
assertEquals(0, NativeStructureSetFilter.exclusionZone(scopedPlacement).stream().count());
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
) {
return DatapackStructureScopeIndex.create(List.of(
new DatapackIngestService.StructureScopeResources(
SOURCE,
structureKeys,
structureSetKeys)));
}
private static Holder<Structure> structureHolder(String key) {
return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null);
}
private static Holder<StructureSet> structureSetHolder(
String key,
Holder<Structure>... structures
) {
return structureSetHolder(
key,
new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345),
structures);
}
private static Holder<StructureSet> structureSetHolder(
String key,
StructurePlacement placement,
Holder<Structure>... structures
) {
List<StructureSet.StructureSelectionEntry> entries = Stream.of(structures)
.map(structure -> new StructureSet.StructureSelectionEntry(structure, 1))
.toList();
StructureSet value = new StructureSet(entries, placement);
return new KeyedHolder<>(
ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)),
value);
}
private static StructureSet structureSet(StructurePlacement placement, String structureKey) {
return new StructureSet(
List.of(new StructureSet.StructureSelectionEntry(
structureHolder(structureKey), 1)),
placement);
}
private static final class KeyedHolder<T> extends Holder.Reference<T> {
private KeyedHolder(ResourceKey<T> key, T value) {
super(Type.STAND_ALONE, new HolderOwner<>() {}, key, null);
bindValue(value);
}
}
private static final class UnsupportedPlacement extends AbstractSpreadingStructurePlacement {
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public MapCodec<? extends AbstractSpreadingStructurePlacement> codec() {
return null;
}
}
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
private MutableStructureSetHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -0,0 +1,252 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePoiUpdates;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.SectionPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructurePoiUpdatesTest {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
doAnswer(call -> {
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
fixture.proto.setBlockState(position, call.getArgument(1), 0);
return true;
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
assertTrue(fixture.updates.setBlock(fixture.world, position,
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
assertArrayEquals(new long[]{16L}, fixture.marker());
}
@Test
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
Fixture fixture = new Fixture();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
}
}
assertArrayEquals(new long[]{16L}, fixture.marker());
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
fixture.loaded.persistentDataContainer = restored;
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
assertFalse(restored.has(DIRTY_SECTIONS));
}
@Test
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
Fixture fixture = new Fixture();
BlockPos retained = new BlockPos(-31, 5, 49);
BlockPos replaced = new BlockPos(-30, 5, 49);
BlockPos removed = new BlockPos(-29, 5, 49);
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
PoiSection existing = new PoiSection.Packed(true, List.of(
new PoiRecord.Packed(retained, farmer, 0),
new PoiRecord.Packed(replaced, farmer, 1),
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
for (BlockPos position : List.of(retained, replaced, removed)) {
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
}
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
fixture.write(removed, Blocks.AIR.defaultBlockState());
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
assertTrue(existing.getType(removed).isEmpty());
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
assertEquals(0, existing.getFreeTickets(retained));
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(0, existing.getFreeTickets(retained));
}
@Test
public void failedReconciliationRetainsIntentForRetry() throws Exception {
Fixture fixture = new Fixture();
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
IllegalStateException failure = new IllegalStateException("POI storage failure");
doThrow(failure).when(fixture.manager).add(any(), any());
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
assertArrayEquals(new long[]{16L}, fixture.marker());
fixture.allowPoiAdds();
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
4096, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
Fixture fixture = new Fixture();
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
onOwner(() -> assertThrows(IllegalArgumentException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
}
private static void onOwner(Runnable action) throws Exception {
FutureTask<Void> task = new FutureTask<>(action, null);
new TickThread(task, "Native POI test").start();
task.get(5L, TimeUnit.SECONDS);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> biome = Holder.direct((Biome) null);
biomeIds.add(biome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
PalettedContainer.codecRW(biomeCodec, biomes, biome));
}
private static final class Fixture {
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
private final LevelChunk loaded = mock(LevelChunk.class);
private final WorldGenLevel world = mock(WorldGenLevel.class);
private final PoiManager manager = mock(PoiManager.class);
private final Map<Long, PoiSection> pois = new HashMap<>();
private Fixture() {
ServerLevel level = mock(ServerLevel.class);
when(level.getPoiManager()).thenReturn(manager);
loaded.persistentDataContainer = proto.persistentDataContainer;
when(loaded.getLevel()).thenReturn(level);
when(loaded.getPos()).thenReturn(proto.getPos());
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
return true;
});
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
when(manager.getType(any())).thenAnswer(call -> {
BlockPos position = call.getArgument(0);
PoiSection section = pois.get(SectionPos.of(position).asLong());
return section == null ? Optional.empty() : section.getType(position);
});
doAnswer(call -> {
BlockPos position = call.getArgument(0);
pois.get(SectionPos.of(position).asLong()).remove(position);
return null;
}).when(manager).remove(any());
allowPoiAdds();
}
private void allowPoiAdds() {
doAnswer(call -> {
BlockPos position = call.getArgument(0);
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
.add(position, call.getArgument(1));
}).when(manager).add(any(), any());
}
private void write(BlockPos position, BlockState state) {
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
}
private long[] marker() {
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
}
private PoiSection records() {
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
}
}
}
@@ -0,0 +1,466 @@
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.util.structure.StructureTerrainMode;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureWorldgenAccessTest {
private static final int GENERATION_CENTER_X = 60;
private static final int GENERATION_CENTER_Z = 15;
private static final int SURFACE_FIRST_FREE_Y = 80;
private static final int FLOOR_FIRST_FREE_Y = 70;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
int farX = (GENERATION_CENTER_X - 9) << 4;
int farZ = (GENERATION_CENTER_Z + 4) << 4;
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
new IrisStructureTerrain().setMode(StructureTerrainMode.FLATTEN)));
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
RecordingDelegate unbounded = new RecordingDelegate();
unbounded.rejectDistantTerrainReads = true;
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
NativeStructurePostProcessor.prepareTerrain(
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
assertEquals(1, recording.heightReads);
}
@Test
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
assertEquals(0, recording.heightReads);
BlockPos local = generationCenter().getMiddleBlockPosition(64);
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
assertEquals(local.atY(90), localProjection.pos());
assertEquals(1, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int x = generationCenter.getMiddleBlockX();
int z = (generationCenter.z() + 2) << 4;
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
assertNull(access.getChunkIfLoadedImmediately(
generationCenter.x(), generationCenter.z() + 2));
List<BlockState> streamed = access.getBlockStates(new AABB(
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
assertEquals(1, streamed.size());
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
generationCenter.getMiddleBlockX(),
FLOOR_FIRST_FREE_Y,
(generationCenter.z() + 2) << 4);
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
access.updateNeighborsAt(position, Blocks.DIRT);
access.neighborShapeChanged(
Direction.NORTH, position, position.north(),
Blocks.DIRT.defaultBlockState(), 2, 512);
access.levelEvent(null, 2001, position, 0);
access.gameEvent(null, Vec3.atCenterOf(position), null);
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(0, recording.mutations);
assertEquals(0, recording.events);
assertEquals(0, recording.blockTicks.count());
}
@Test
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
(generationCenter.x() + 1) << 4,
FLOOR_FIRST_FREE_Y,
(generationCenter.z() - 1) << 4);
assertTrue(access.isInsideGenerationRegion(
generationCenter.x() + 1, generationCenter.z() - 1));
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertEquals(91, access.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
assertTrue(access.ensureCanWrite(position));
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertTrue(access.removeBlock(position, false));
assertTrue(access.destroyBlock(position, false));
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(1, recording.terrainReads);
assertEquals(1, recording.heightReads);
assertEquals(4, recording.mutations);
assertEquals(1, recording.blockTicks.count());
}
@Test
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ChunkPos generationCenter = generationCenter();
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertEquals(0, recording.heightReads);
}
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
BlockPos position = generationCenter().getMiddleBlockPosition(70);
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
assertNull(access.getBlockEntity(position));
assertTrue(access.getBlockEntity(position, null).isEmpty());
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
access.updateNeighborsAt(position.east(), Blocks.DIRT);
access.neighborShapeChanged(
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
assertEquals(1, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(0, recording.blockTicks.count());
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
assertEquals(1, recording.mutations);
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
assertNull(access.getChunk(generationCenter.getWorldPosition()));
assertEquals(1, recording.statuslessChunkReads);
assertEquals(0, recording.statusChunkReads);
}
@Test
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int safeMaxX = generationCenter.getMaxBlockX() + 17;
int safeMinZ = generationCenter.getMinBlockZ() - 16;
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
AABB disjoint = new AABB(
safeMaxX, -64, safeMinZ,
safeMaxX + 16, 320, safeMaxZ);
AABB overlap = new AABB(
safeMaxX - 8, -128, safeMinZ - 8,
safeMaxX + 16, 400, safeMaxZ + 8);
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
access.getEntities((Entity) null, overlap, entity -> true);
access.getEntities(type, overlap, entity -> true);
assertEquals(2, recording.entityAreas.size());
for (AABB delegatedArea : recording.entityAreas) {
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
assertEquals(-64, delegatedArea.minY, 0D);
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
assertEquals(320, delegatedArea.maxY, 0D);
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
}
}
private static NativeStructureWorldgenAccess access(RecordingDelegate recording) {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
}
private static ChunkPos generationCenter() {
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
}
private static final class RecordingDelegate implements InvocationHandler {
private final Holder<Biome> biome;
private final BiomeManager biomeManager;
private final RecordingTicks<Block> blockTicks;
private final RecordingTicks<Fluid> fluidTicks;
private final List<AABB> entityAreas;
private int terrainReads;
private int heightReads;
private int chunkReads;
private int statuslessChunkReads;
private int statusChunkReads;
private int mutations;
private int events;
private boolean rejectDistantTerrainReads;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
this.blockTicks = new RecordingTicks<>();
this.fluidTicks = new RecordingTicks<>();
this.entityAreas = new ArrayList<>();
}
private WorldGenLevel world() {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) {
String name = method.getName();
if (name.equals("getSeaLevel")) {
return 63;
}
if (name.equals("getBiome")) {
return biome;
}
if (name.equals("getBiomeManager")) {
return biomeManager;
}
if (name.equals("getBlockTicks")) {
return blockTicks;
}
if (name.equals("getFluidTicks")) {
return fluidTicks;
}
if (name.equals("getMinY")) {
return -64;
}
if (name.equals("getHeight") && arguments == null) {
return 384;
}
if (name.equals("getHeight")) {
if (rejectDistantTerrainReads
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
throw new IllegalStateException("Requested chunk unavailable during world generation");
}
heightReads++;
return 91;
}
if (name.equals("getBlockState")) {
terrainReads++;
return Blocks.DIRT.defaultBlockState();
}
if (name.equals("getFluidState")) {
terrainReads++;
return Blocks.WATER.defaultBlockState().getFluidState();
}
if (name.equals("getChunk")) {
chunkReads++;
if (arguments.length == 2) {
statuslessChunkReads++;
} else {
statusChunkReads++;
}
return null;
}
if (name.equals("getChunkIfLoadedImmediately")) {
chunkReads++;
return null;
}
if (name.equals("getEntities")) {
entityAreas.add((AABB) arguments[1]);
return List.of();
}
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
mutations++;
return true;
}
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
mutations++;
return null;
}
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
events++;
return null;
}
if (name.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (name.equals("equals")) {
return proxy == arguments[0];
}
if (name.equals("toString")) {
return "native-structure-worldgen-test-delegate";
}
throw new UnsupportedOperationException(method.toString());
}
}
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
private int count;
@Override
public void schedule(ScheduledTick<T> tick) {
count++;
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return false;
}
@Override
public int count() {
return count;
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return false;
}
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NmsChunkIOFlushTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
Fixture fixture = new Fixture();
AtomicBoolean chunkPending = new AtomicBoolean(true);
AtomicBoolean poiPending = new AtomicBoolean(true);
AtomicBoolean entityPending = new AtomicBoolean(true);
CountDownLatch chunkDrain = new CountDownLatch(1);
CountDownLatch poiDrain = new CountDownLatch(1);
CountDownLatch entityDrain = new CountDownLatch(1);
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
chunkDrain.countDown();
return chunkPending.get();
});
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
poiDrain.countDown();
return poiPending.get();
});
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
entityDrain.countDown();
return entityPending.get();
});
ExecutorService io = Executors.newSingleThreadExecutor();
try {
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
chunkPending.set(false);
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
poiPending.set(false);
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
entityPending.set(false);
drain.get(5L, TimeUnit.SECONDS);
} finally {
chunkPending.set(false);
poiPending.set(false);
entityPending.set(false);
io.shutdownNow();
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
}
}
@Test
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("native IO failed");
when(fixture.chunks.hasTasks()).thenThrow(failure);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> fixture.binding.flushChunkIO(fixture.world)));
}
private static final class Fixture {
private final NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
private final CraftWorld world = mock(CraftWorld.class);
private final ServerLevel level = mock(ServerLevel.class);
private final RegionDataController chunks = mock(RegionDataController.class);
private final RegionDataController pois = mock(RegionDataController.class);
private final RegionDataController entities = mock(RegionDataController.class);
private Fixture() {
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
}
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsChunkRetentionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
}
@Test
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
ChunkHolderManager manager = mock(ChunkHolderManager.class);
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
managerField.setAccessible(true);
managerField.set(scheduler, manager);
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
assertTrue(binding.retainChunk(world, 3, -4));
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
assertTrue(type.getValue().doesLoad());
assertFalse(type.getValue().doesSimulate());
assertFalse(type.getValue().persist());
assertTrue(binding.releaseChunk(world, 3, -4));
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
binding.retainChunk(world, 3, -4);
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertSame(type.getValue(), again.getValue());
}
}
@@ -0,0 +1,96 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
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.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class NmsChunkTaskPollingTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void serverOwnerPumpsOnlyTheTargetWorldQueue() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CompletableFuture<Void> completed = new CompletableFuture<>();
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(server.getRunningThread()).thenReturn(Thread.currentThread());
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
assertTrue(binding.pollChunkTask(world));
assertTrue(completed.isDone());
assertFalse(binding.pollChunkTask(world));
}
verify(chunks, times(2)).pollTask();
verify(server, never()).executeAllRecentInternalTasks();
}
@Test
public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world));
}
verify(level, never()).getChunkSource();
}
@Test
public void foliaNeverUsesTheWorldWideTaskPump() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true);
assertFalse(binding.pollChunkTask(world));
}
verifyNoInteractions(world);
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import org.bukkit.Difficulty;
import org.bukkit.Location;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsEntitySpawnDifficultyTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
int rejected = 0;
for (EntityType type : EntityType.values()) {
if (type.getEntityClass() == null) {
continue;
}
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
continue;
}
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
rejected++;
}
assertTrue(rejected > 20);
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
}
@Test
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
CraftWorld world = mock(CraftWorld.class);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
when(world.getDifficulty()).thenReturn(difficulty);
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void unrelatedBukkitSpawnFailuresStillPropagate() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
}
}
@@ -0,0 +1,353 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainPipeline;
import art.arcane.iris.nativegen.v26_3_R1.NativeBlockTags;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class WorldgenTerrainHeightmapsTest {
private static final int MIN_Y = -64;
private static final int HEIGHT = 384;
private static final int LAND_TERRAIN_TOP = 70;
private static final int LAND_CANOPY_TOP = 80;
private static final int OCEAN_FLOOR_TOP = 60;
private static final int OCEAN_FLUID_TOP = 64;
private static final int LAND_X = 3;
private static final int LAND_Z = 5;
private static final int OCEAN_X = 8;
private static final int OCEAN_Z = 8;
private static final int TERRAIN_SLAB_DEPTH = 4;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
NativeTerrainPipeline.primeAuthoringHeightmaps(chunk);
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP + 6,
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
}
@Test
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
WorldGenLevel world = world(chunks);
StructureStart start = startInOrigin();
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
assertEquals(LAND_CANOPY_TOP, canopySnap);
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
}
@Test
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
for (int chunkX = -1; chunkX <= 1; chunkX++) {
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
}
}
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
for (ChunkAccess chunk : chunks.values()) {
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
}
@Test
public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0));
ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
chunks.put(ChunkPos.pack(1, 0), neighbour);
chunks.put(ChunkPos.pack(2, 0), distanceTwo);
WorldGenLevel world = world(chunks);
StructureStart shifted = startInOrigin();
shifted.getPieces().forEach(piece -> piece.move(16, 0, 0));
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(shifted),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
}
@Test
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
private static int iglooSnapOffset(WorldGenLevel world) {
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
- IglooPieces.GENERATION_HEIGHT - 1;
}
private static StructureStart startInOrigin() {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static IntBinaryOperator surfaceFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static IntBinaryOperator floorFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static boolean isOcean(int x, int z) {
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
}
private static ProtoChunk terrainChunk(ChunkPos pos) {
ProtoChunk chunk = new ProtoChunk(
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
containerFactory(), null);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
}
if (ocean) {
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
}
continue;
}
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
}
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
}
}
}
return chunk;
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("hasChunk")) {
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
}
if (methodName.equals("getChunk") && arguments.length == 2) {
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
if (chunk == null) {
throw new AssertionError("Heightmap priming requested an unavailable chunk "
+ arguments[0] + "," + arguments[1]);
}
return chunk;
}
if (methodName.equals("getHeight") && arguments.length == 3) {
Heightmap.Types type = (Heightmap.Types) arguments[0];
int x = (int) arguments[1];
int z = (int) arguments[2];
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
if (chunk == null) {
throw new AssertionError("Heightmap read requested an unavailable chunk at "
+ x + "," + z);
}
return chunk.getHeight(type, x & 15, z & 15) + 1;
}
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
if (chunk == null) {
throw new AssertionError("Block read requested an unavailable chunk at " + position);
}
return chunk.getBlockState(position);
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "worldgen-heightmap-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
}
@@ -0,0 +1,160 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.world.history.GenerationRegistryContract;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public final class BareMobSpawnHistoryTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
new GenerationRegistryContract.PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/" + "c".repeat(64)
);
private static HeadlessNativeRegistries loaded;
private static PlatformGenerationRegistry registry;
@BeforeClass
public static void installOverlayRendering() throws IOException {
assertNull(Bukkit.getServer());
loaded = HeadlessNativeRegistries.load(Map.of(
"data/iris/worldgen/biome/biomes/" + "c".repeat(64) + ".json",
spawningBiome().generateJson(new DataFixerV263())
));
registry = new HeadlessNativePlatform(new File("unused-native-platform"), loaded::registries)
.generationRegistry();
}
@AfterClass
public static void closeRegistries() throws IOException {
if (loaded != null) {
loaded.close();
}
}
@Test
public void bareMobSpawnHistoryValidatesAgainstTheInstalledOverlayRendering() throws IOException {
GenerationRegistryContract legacy = legacyContract(INSTALLED, spawningBiome());
GenerationRegistryContract.GeneratedSource source = legacy.generatedSources().get(INSTALLED);
assertEquals(GenerationRegistryContract.FINGERPRINT_SCHEMA_VERSION_ONE, legacy.fingerprintSchema());
assertEquals(LEGACY_RENDERER_IDENTITY, source.rendererIdentity());
assertNotEquals(
JsonParser.parseString(source.sourceJson()),
JsonParser.parseString(registry.generatedDefinition(
INSTALLED.registryKey(),
INSTALLED.resourceKey()
).value())
);
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(legacy),
new DataFixerV263(),
registry
);
legacy.requireDefinitionsAvailableIn(available);
}
@Test
public void bareMobSpawnHistoryStillRejectsDifferentSemantics() throws IOException {
GenerationRegistryContract changed = legacyContract(INSTALLED, spawningBiome().setTemperature(0.2D));
IOException failure = assertThrows(IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(changed),
new DataFixerV263(),
registry
));
assertEquals("Historical generated registry definition changed for minecraft:worldgen/biome / "
+ INSTALLED.resourceKey() + ".", failure.getMessage());
}
@Test
public void bareMobSpawnHistoryStillRejectsAMissingDefinition() throws IOException {
GenerationRegistryContract missing = legacyContract(new GenerationRegistryContract.PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/" + "d".repeat(64)
), spawningBiome());
assertThrows(IOException.class, () -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(missing),
new DataFixerV263(),
registry
));
}
private static GenerationRegistryContract legacyContract(
GenerationRegistryContract.PhysicalResourceKey key,
IrisBiomeCustom biome
) {
JsonObject rendered = JsonParser.parseString(biome.generateJson(new DataFixerV263())).getAsJsonObject();
JsonObject attributes = rendered.getAsJsonObject("attributes");
attributes.add(NATURAL_MOB_SPAWNS, attributes.getAsJsonObject(NATURAL_MOB_SPAWNS).get("argument"));
String captured = registry.canonicalDefinition(key.registryKey(), key.resourceKey(), rendered.toString())
.value();
String semantic = GenerationRegistryContractFactory.customBiomeEffectiveSemanticJson(biome, null);
GenerationRegistryContract.GeneratedSource source = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
GenerationRegistryContractFactory.fingerprintCustomBiomeAuthoredDefinition(semantic),
semantic,
LEGACY_RENDERER_IDENTITY,
GenerationRegistryContractFactory.fingerprintDefinition(
key,
PlatformGenerationRegistry.Definition.exactJson(captured),
"generated"
),
captured
);
return GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, source)),
Map.of(key, source)
);
}
private static IrisBiomeCustom spawningBiome() {
return new IrisBiomeCustom()
.setId("tester")
.setSpawnRarity(2)
.setSpawns(new KList<>(
new IrisBiomeCustomSpawn()
.setType("minecraft:zombie")
.setGroup(IrisBiomeCustomSpawnType.MONSTER)
.setMinCount(2)
.setMaxCount(4)
.setWeight(7),
new IrisBiomeCustomSpawn()
.setType("minecraft:cow")
.setGroup(IrisBiomeCustomSpawnType.CREATURE)
.setMinCount(3)
.setMaxCount(3)
.setWeight(5)
));
}
}
@@ -0,0 +1,235 @@
package art.arcane.iris.probe;
import art.arcane.iris.probe.NativeRegionPlan.PlannedChunk;
import net.minecraft.nbt.CompoundTag;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.GenerationChunkHolder;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.StaticCache2D;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.ChunkStep;
import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.concurrent.atomic.AtomicInteger;
final class DetachedNativeTerrain {
private static final NamespacedKey NATURAL_TERRAIN = new NamespacedKey("iris", "natural_terrain");
private DetachedNativeTerrain() {
}
static Result generate(Request request, Consumer<String> progress) throws Exception {
List<PlannedChunk> plan = NativeRegionPlan.plan(request.chunkX(), request.chunkZ(), request.width());
Path output = Files.createDirectory(request.output());
WorldGenContext context = context(request.level().getChunkSource().chunkMap);
Map<Long, DetachedHolder> holders = new HashMap<>(plan.size());
for (PlannedChunk planned : plan) {
ChunkPos position = new ChunkPos(planned.x(), planned.z());
holders.put(position.pack(), new DetachedHolder(new ProtoChunk(position, UpgradeData.EMPTY,
request.level(), request.level().palettedContainerFactory(), null)));
}
long started = System.nanoTime();
try (RegionGenerationWindow workers = new RegionGenerationWindow(request.parallelism(), RegionGenerationWindow.Policy.EMBEDDED)) {
for (ChunkStatus status : ChunkStatus.getStatusList()) {
if (status == ChunkStatus.EMPTY) {
continue;
}
if (status.isAfter(ChunkStatus.TERRAIN)) {
break;
}
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
executeStage(new Stage(context, step, plan, holders, request.parallelism()), workers, progress);
}
}
long serializationStarted = System.nanoTime();
write(request.level(), output, plan, holders);
progress.accept("stage=native_serialization seconds=" + (System.nanoTime() - serializationStarted) / 1_000_000_000.0);
double seconds = (System.nanoTime() - started) / 1_000_000_000.0;
return new Result(request.width() * request.width(), plan.size(), seconds);
}
private static void executeStage(Stage stage, RegionGenerationWindow workers, Consumer<String> progress) throws Exception {
long started = System.nanoTime();
List<PlannedChunk> eligible = new ArrayList<>();
for (PlannedChunk planned : stage.plan()) {
if (planned.status().isOrAfter(stage.step().targetStatus())) {
eligible.add(planned);
}
}
AtomicInteger completed = new AtomicInteger();
AtomicInteger targetCompleted = new AtomicInteger();
workers.process(new RegionGenerationWindow.Request<>(eligible.size(), stage.parallelism(),
index -> {
PlannedChunk planned = eligible.get(index);
apply(stage, planned);
return planned;
}, (index, planned) -> {
int count = completed.incrementAndGet();
if (planned.target()) {
targetCompleted.incrementAndGet();
}
if (count % 128 == 0) {
progress.accept("stage=" + stage.step().targetStatus().getName() + " completed=" + count
+ " elapsedSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
}
}));
progress.accept("stage=" + stage.step().targetStatus().getName() + " chunks=" + completed.get()
+ " targetChunks=" + targetCompleted.get() + " haloChunks=" + (completed.get() - targetCompleted.get())
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
}
private static void apply(Stage stage, PlannedChunk planned) {
DetachedHolder center = requireHolder(stage.holders(), planned.x(), planned.z());
StaticCache2D<GenerationChunkHolder> cache = StaticCache2D.create(planned.x(), planned.z(),
stage.step().directDependencies().size() - 1, (x, z) -> requireHolder(stage.holders(), x, z));
ChunkAccess result = stage.step().apply(stage.context(), cache, center.chunk).join();
if (result != center.chunk || result.getPersistedStatus() != stage.step().targetStatus()) {
throw new IllegalStateException("Native stage did not retain its detached chunk: " + stage.step().targetStatus());
}
}
private static DetachedHolder requireHolder(Map<Long, DetachedHolder> holders, int x, int z) {
DetachedHolder holder = holders.get(ChunkPos.pack(x, z));
if (holder == null) {
throw new IllegalStateException("Native dependency escaped the terrain halo at " + x + "," + z);
}
return holder;
}
private static WorldGenContext context(ChunkMap map) throws ReflectiveOperationException {
for (Field field : ChunkMap.class.getDeclaredFields()) {
if (field.getType() == WorldGenContext.class) {
field.setAccessible(true);
return (WorldGenContext) field.get(map);
}
}
throw new IllegalStateException("Native world generation context is unavailable");
}
private static void write(ServerLevel level, Path output, List<PlannedChunk> plan,
Map<Long, DetachedHolder> holders) throws IOException {
List<PlannedChunk> ordered = new ArrayList<>(plan);
ordered.sort(Comparator.comparingInt((PlannedChunk chunk) -> chunk.z() >> 5)
.thenComparingInt(chunk -> chunk.x() >> 5).thenComparingInt(PlannedChunk::z).thenComparingInt(PlannedChunk::x));
StringBuilder receipts = new StringBuilder();
RegionFile region = null;
long currentRegion = Long.MIN_VALUE;
try {
for (PlannedChunk planned : ordered) {
if (planned.status() == ChunkStatus.EMPTY) {
continue;
}
long key = ChunkPos.pack(planned.x() >> 5, planned.z() >> 5);
if (key != currentRegion) {
if (region != null) {
region.close();
}
Path file = output.resolve("r." + (planned.x() >> 5) + "." + (planned.z() >> 5) + ".mca");
region = new RegionFile(new RegionStorageInfo(level.serverLevelData.getLevelName(),
level.dimension(), "chunk"), file, output, false);
currentRegion = key;
}
ChunkAccess chunk = holders.get(ChunkPos.pack(planned.x(), planned.z())).chunk;
if (planned.status() == ChunkStatus.TERRAIN) {
receipts.append(planned.x()).append(",").append(planned.z()).append("=")
.append(receiptHash(chunk)).append("\n");
}
CompoundTag tag = SerializableChunkData.copyOf(level, chunk).write();
NativeRegionTerrainWriter.write(region, tag);
}
Files.writeString(output.resolve("receipts.properties"), receipts.toString(), StandardCharsets.UTF_8);
} finally {
if (region != null) {
region.close();
}
}
}
static String receiptHash(ChunkAccess chunk) {
byte[] receipt = chunk.persistentDataContainer.get(NATURAL_TERRAIN,
PersistentDataType.BYTE_ARRAY);
if (receipt == null || receipt.length == 0) {
throw new IllegalStateException("Missing Iris natural terrain receipt at " + chunk.getPos());
}
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(receipt));
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException(failure);
}
}
record Request(ServerLevel level, int chunkX, int chunkZ, int width, int parallelism, Path output) {
Request {
if (parallelism < 1 || parallelism > 16) {
throw new IllegalArgumentException("Native terrain worker count must be 1..16");
}
}
}
private record Stage(WorldGenContext context, ChunkStep step, List<PlannedChunk> plan,
Map<Long, DetachedHolder> holders, int parallelism) {
}
record Result(int terrainChunks, int dependencyChunks, double seconds) {
}
private static final class DetachedHolder extends GenerationChunkHolder {
private final ProtoChunk chunk;
private DetachedHolder(ProtoChunk chunk) {
super(chunk.getPos());
this.chunk = chunk;
}
@Override
public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus status) {
if (chunk.getPersistedStatus().isBefore(status)) {
throw new IllegalStateException("Native dependency is not ready: " + getPos() + " needs " + status);
}
return chunk;
}
@Override
protected void addSaveDependency(CompletableFuture<?> dependency) {
throw new UnsupportedOperationException("Detached chunks have no live save queue");
}
@Override
public int getTicketLevel() {
throw new UnsupportedOperationException("Detached chunks have no live tickets");
}
@Override
public int getQueueLevel() {
throw new UnsupportedOperationException("Detached chunks have no live queue");
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.probe;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.ChunkStep;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public final class DetachedNativeTerrainTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void fullRegionPlanSatisfiesEveryNativeStageDependency() {
List<NativeRegionPlan.PlannedChunk> plan = NativeRegionPlan.plan(-32, 64, 32);
Map<Long, ChunkStatus> statuses = new HashMap<>();
int terrain = 0;
for (NativeRegionPlan.PlannedChunk chunk : plan) {
statuses.put(ChunkPos.pack(chunk.x(), chunk.z()), chunk.status());
if (chunk.status() == ChunkStatus.TERRAIN) {
terrain++;
}
}
assertEquals(1296, terrain);
assertEquals(2916, plan.size());
assertTrue(plan.size() < 4096);
for (NativeRegionPlan.PlannedChunk chunk : plan) {
for (ChunkStatus status : ChunkStatus.getStatusList()) {
if (status == ChunkStatus.EMPTY || status.isAfter(chunk.status())) {
continue;
}
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
int radius = step.directDependencies().size() - 1;
for (int z = -radius; z <= radius; z++) {
for (int x = -radius; x <= radius; x++) {
ChunkStatus required = step.directDependencies().get(Math.max(Math.abs(x), Math.abs(z)));
ChunkStatus available = statuses.get(ChunkPos.pack(chunk.x() + x, chunk.z() + z));
assertTrue("Missing native stage dependency", available != null && available.isOrAfter(required));
}
}
}
}
}
}
@@ -0,0 +1,149 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeSourceImpl;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
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.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class HeadlessChunkGeneratorTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void nativeStructureStateAndOriginMatchThePaperGeneratorDefaults() throws Exception {
int checked = 0;
for (Method method : CustomChunkGenerator.class.getMethods()) {
if (!method.getName().equals("getOrigin") && !method.getName().equals("createState")) {
continue;
}
assertEquals(ChunkGenerator.class, method.getDeclaringClass());
assertEquals(method, HeadlessChunkGenerator.class.getMethod(method.getName(), method.getParameterTypes()));
checked++;
}
assertEquals(2, checked);
}
@Test
public void nativeSwampHutQueriesRealPackHeightsAndBiomesWithoutAServer() throws Exception {
long seed = 69420L;
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
pack().toFile(), "main", "[native-column-test]", runtime, temporary.getRoot().toPath()));
RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "columns");
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
temporary.newFolder().toPath()).createAccess("native")) {
Engine engine = session.engine();
HeadlessChunkGenerator generator = spy(new HeadlessChunkGenerator(
new HeadlessChunkGenerator.Options(engine, runtime.registries())));
assertTrue(generator.getBiomeSource() instanceof NativeBiomeSourceImpl);
assertEquals(engine.getMinHeight(), generator.getMinY());
assertEquals(engine.getHeight(), generator.getGenDepth());
assertEquals(engine.getMinHeight() + engine.getDimension().getFluidHeight(), generator.getSeaLevel());
Holder<NoiseGeneratorSettings> settings = runtime.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(NoiseGeneratorSettings.OVERWORLD);
RandomState random = RandomState.create(runtime.registries().lookupOrThrow(Registries.NOISE), seed, settings.value());
LevelHeightAccessor height = LevelHeightAccessor.create(generator.getMinY(), generator.getGenDepth());
Holder<Biome> swamp = runtime.registries().lookupOrThrow(Registries.BIOME).getOrThrow(
ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:swamp")));
assertTrue(generator.getBiomeSource().possibleBiomes().contains(swamp));
for (int x : new int[]{-33, 0, 49}) {
int z = -x - 7;
int expected = engine.getMinHeight() + engine.getHeight(x, z, true) + 1;
assertTrue(expected > generator.getSeaLevel());
assertEquals(expected, generator.getBaseHeight(x, z, Heightmap.Types.OCEAN_FLOOR_WG, height, random));
NoiseColumn column = generator.getBaseColumn(x, z, height, random);
assertFalse(column.getBlock(expected - 1).isAir());
assertTrue(column.getBlock(expected).isAir());
assertEquals(swamp, generator.getBiomeSource().createResolver(
random.createClimateSampler(SamplerContext.builder().enableCaches().build()))
.getNoiseBiome(x >> 2, expected >> 2, z >> 2));
}
Holder.Reference<Structure> hut = runtime.registries().lookupOrThrow(Registries.STRUCTURE).getOrThrow(
ResourceKey.create(Registries.STRUCTURE, Identifier.parse("minecraft:swamp_hut")));
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
ChunkPos position = new ChunkPos(-3, 2);
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY, height,
PalettedContainerFactory.create(runtime.registries()), null);
clearInvocations(generator);
StructureStart start = hut.value().generate(hut, Level.OVERWORLD, runtime.registries(), generator,
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()),
random, templates, seed, position, 0, chunk, hut.value().biomes()::contains);
assertTrue(start.isValid());
assertFalse(start.getPieces().isEmpty());
verify(generator, atLeastOnce()).getBaseHeight(anyInt(), anyInt(), any(Heightmap.Types.class),
any(LevelHeightAccessor.class), any(RandomState.class));
StructureStart excluded = hut.value().generate(hut, Level.OVERWORLD, runtime.registries(), generator,
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()),
random, templates, seed, position, 0, chunk, selected -> !selected.equals(swamp));
assertFalse(excluded.isValid());
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
}
}
private Path pack() throws Exception {
Path pack = temporary.newFolder().toPath();
for (String directory : List.of("dimensions", "regions", "biomes", "generators")) {
Files.createDirectories(pack.resolve(directory));
}
Files.writeString(pack.resolve("dimensions/main.json"), """
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
""");
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
Files.writeString(pack.resolve("biomes/swamp.json"), """
{"derivative":"minecraft:swamp","generators":[{"generator":"flat","min":35,"max":35}]}
""");
return pack;
}
}
Loaded 100 of 1908 files, more files were not shown because too many files have changed in this diff. Show more