Compare commits

..
757 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
Brian Neumann-Fopiano b18681a3e2 Command Corrections. 2026-09-17 04:48:35 -04:00
Brian Neumann-Fopiano 1e1eaa96aa Hydrology Fix, and V+ 2026-09-15 23:01:56 -04:00
Brian Neumann-Fopiano dfb3748059 Adapter and papi fixes 2026-09-13 21:27:44 -04:00
Brian Neumann-Fopiano c14a972ab8 Fixed Maven publishing
- ItemsAdder API resolves from Maven Central
- Integration repositories only resolve their assigned groups and modules
- Shared VolmLib classes and packaging use the same pinned revision
- JitPack builds and publishes with remote dependencies
- Local Maven publication uses art.arcane:iris with the configured version
- API generation tracks the final archive filename lazily
- API jars retain signatures and metadata with valid throwing method stubs
- Publication tests verify metadata, bytecode, and consumer compilation
2026-09-13 17:05:05 -04:00
Brian Neumann-Fopiano 9a1e4b91d1 Generation and runtime fixes
- Surface detail blends raw noise with cached six-block interpolation
- Locked biome layers sample only the requested ranges
- Surface layers skip noise when slope requirements fail
- Stilt supports exclude configured materials and block entities
- Ice and glass remain eligible foundation materials
- Block-drop routing filters materials before loading biome history
- Vision displays biome previews before river refinement
- Interrupted previews retain reusable partial tiles
- Hydrology waiters stop without cancelling shared planning tasks
- Regression tests cover generation, supports, drop routing, and preview cancellation
2026-09-13 17:00:33 -04:00
Brian Neumann-Fopiano e3c6a32968 Propere types 2026-09-13 12:53:43 -04:00
Brian Neumann-Fopiano ef26f3f443 Correcting merge conflit 2026-09-12 17:57:24 -04:00
Brian Neumann-Fopiano 7a099cbd5e Merge Back 2026-09-12 16:06:08 -04:00
Robert Stoneking e832239730 Fixed language reload fallback
- Invalid language updates retain the last valid messages
- Malformed startup catalogs use English defaults
- Unrelated reloads preserve personal language caches
- Deleted locales use English without changing saved choices
- Restored locale files resume their translations
- Language catalogs include shared editor titles and remove obsolete usage messages
2026-09-12 13:53:11 -06:00
Brian Neumann-Fopiano bc9b7e86d7 Merge debug command reorganization
- Swift's debug command tree lands on the consolidated package layout
- CommandDebug and its tests move from core.commands to command
2026-09-11 21:47:30 -04:00
Brian Neumann-Fopiano 3f57925443 Consolidate source layout
- Packages regrouped by domain: generation, world, pack, studio, structure, platform, command, localization, diagnostics
- Forked VolmLib utilities dropped for the shared library: noise, stream, interpolation, hunk, matter
- Engine-coupled stream decorators moved to generation.stream.GenerationStreams
- Hunk section splits now run on the Iris burst pool through HunkBurst
- Artifact entry lists, purity allowlists and shrink rules follow the new paths
2026-09-11 21:46:22 -04:00
Robert Stoneking 251b0e250d Reorganized debug commands
- Debug dump replaces the root debugdump command
- Debug toggle controls diagnostic logging
- Debug version uses the shared version display
- Root version remains executable but hidden from help
- Diagnostic dump routing retains its separate permission path
- Translations and tests cover the revised command structure
2026-09-11 17:21:04 -06:00
Brian Neumann-Fopiano 29a31a69e0 Patching Cleanup 2026-09-11 00:38:08 -04:00
Brian Neumann-Fopiano bf12f30d26 Hydrology fixes 2026-09-10 23:07:46 -04:00
Brian Neumann-Fopiano 577f924133 Merge remote-tracking branch 'origin/master' 2026-09-10 13:08:14 -04:00
Brian Neumann-Fopiano e4cdf2dcda Patches 2026-09-10 13:08:12 -04:00
Robert Stoneking 5ba95388f8 Merge remote-tracking branch 'origin/master' 2026-09-10 01:17:34 -06:00
Robert Stoneking 45407a7e3f Updated Iris language files -
All 17 translations now use editable TOML files
Language menus and debug messages include translations
Headers explain all 243 variables, prefixes, colors and formatting
Generated guides refresh without overwriting customized messages
Missing or invalid messages fall back to English
Language reloads preserve server and player selections
Jars include localized guides and required TOML support
2026-09-10 01:17:18 -06:00
Brian Neumann-Fopiano e33e6a632a NBative Structure fixes.
- This fixes the native structures that are in iris from generating with pillaric problems
2026-09-10 02:15:20 -04:00
Brian Neumann-Fopiano 62e61970f8 Production-readiness suite fixes
Surface hydrology policy split, native terrain height cache, watched-edit
hotload cancellation, world-save requestSave with tracked ownership flush,
background-task drain that follows saves admitted mid-drain, peaceful spawn
guard, cached-completion terminal state, IO drains off the owner thread.

Studio pregen at scale: detached historical runtimes keep the configured
noise cache instead of the Studio-wide cache; retired runtimes are no longer
pinned by the volumetric fragment filter's per-thread scratch; pregen mantle
cleanup and native region flushes run on per-run threads instead of the
server thread and the shared Iris IO pool (the flush on the IO pool starved
tectonic plate loads and wedged every Paper worker in Mantle.getChunks);
background-task cancellation claims tasks atomically.
2026-09-09 16:24:05 -04:00
Brian Neumann-Fopiano 4de1aea8b9 CrantEngine Tweak and a bit more.
- Fixed CE support
- Added some checks for things like paint and so on in the 3d Generator.
2026-09-08 19:22:55 -04:00
Brian Neumann-Fopiano 0782bbf0b4 Whoops Core 2026-09-08 16:15:19 -04:00
Brian Neumann-Fopiano 98e51cba8d Merge master into 3D terrain
- Resolve IrisTerrainNormalActuator by keeping master's primitive riverWaterSurfaceStream lookup and primarySurfaceLayerOrNull accessors while retaining the terrain column, span, layer surface and ceiling locals that write air through terrain3D gaps
- Resolve HydrologyFootprintCompiler by keeping the MaterializedSurface planned-surface record with terrain ownership beside master's package-private SurfaceRasterIndex, which retains its ownsTerrain override
- Resolve HydrologyCaveContainmentPlanner by dropping the nested RecordingCaveVoxelView, SurfaceObservations and ViewObservations types that master promoted to their own files
- Port the surface-observation ownership check into CaveSurfaceObservations so cached cave validations replay both height and ownership observations through Observation.matches
- Resolve PackExportClosureTest by keeping master's retirement of the source-text packager guard while retaining the snippet export tests, and restore the Files and Path imports those tests use
- Resolve IrisBiomeLayerSlopeSourceTest by folding the lower-ledge slope palette case into master's lockLayers parameterization
- Recapture the studio schema digest for the terrain3D biome property, its IrisTerrain3D definition, the terrain-3d snippet enumeration and the reworded upperDimensionCarving description, verified by reconstructing the previous digest from the stripped schema
2026-09-08 13:58:00 -04:00
Brian Neumann-Fopiano c39a6403bc Volumetric surface terrain
- Add an optional per-biome `terrain3D` profile with density and fissure noise styles, amplitude, horizontal and vertical feature scales, crack depth, width and scale, slope and fluid-clearance gates, and a seed offset, each range-checked before use
- Add a terrain runtime that samples the signed density and fissure fields on a four-block lattice around the biome's generated height and resolves each column into solid spans, so surfaces gain projecting rock, undercuts, ledges and narrow cracks that fade out on flat ground and near the fluid line
- Remove unsupported rock by flood-filling span components across neighbouring columns and dropping any fragment of 512 blocks or fewer, while larger connected masses are kept
- Report the shaped column top as the natural terrain height, keep the unshaped generator height available as a separate stream, and measure surface slope between neighbouring ledge floors instead of the flat heightmap
- Write air through the gaps in the normal terrain actuator, generate a fresh surface palette for every ledge floor, and paint ceiling layers beneath undercuts
- Carry each layer's own column through the dimension stack and upper dimension so stack top heights, solidity and surface lookups follow the spans, and each exposed face draws its palette from its own source height and slope
- Sample a layer's slope condition once per column at the surface being generated, and gate surface decorators on the slope of the ledge they sit on rather than the column heightmap
- Decorate every exposed ledge floor and the ceiling above it during the decorant pass
- Treat shaped openings as surface rather than cave when resolving biomes, skip them during cave carving, suppress cave zone markers inside them, and resolve their boundary biome from the surface biome stream
- Include shaped openings in mantle carve queries and carved columns, fall back to terrain solidity for surface-solid checks across stack, upper and normal terrain, and answer both queries from the island mask when placing objects on floating islands
- Let the hydrology planned surface report whether it owns a column, so the cave voxel view consults natural terrain solidity where hydrology does not own the terrain, and split replayed plan observations into height and ownership records
- Validate `terrain3D` on biomes and `snippet/terrain-3d` snippets as a blocking pack error, rejecting unknown fields, wrong types, out-of-range numbers, unknown noise styles, style nesting past 32 levels and snippet references that escape the pack
- Copy the whole snippet tree during modded studio pack export through one shared routine instead of only `snippet/style`, so terrain and nested style snippets survive export
- Add a terrain probe and Gradle task that generates real pack chunks, compares predicted column geometry against the actual blocks, requires retained overhang coverage for named biomes, reports detached-component topology, and writes cross-section images with a JSON summary
- Enumerate image-mapped regions and skip compat-excluded ones when collecting biomes for stacked and upper dimension terrain
2026-09-08 13:26:33 -04:00
Brian Neumann-Fopiano 425d305d0b Drop JIT-bound allocation assertion
- Remove the accessor allocation comparison whose per-call byte counts depend on escape analysis timing and failed under load, keeping the three behavioral accessor tests
2026-09-08 06:36:17 -04:00
Brian Neumann-Fopiano 114cc68a08 Extend VolmLib interpolation
- Make IrisInterpolation extend the VolmLib IrisInterpolation and delete the nine numerically identical statics it duplicated: hermite, cubic, bicubic, bihermite, trihermite, tricubic, lerp, blerp and trilerp
- Keep the Iris rangeScale because the VolmLib copy short-circuits bmax == bmin and generation output must not move
- Ship the VolmLib IrisInterpolation and InterpolationMethod by removing their unusedVolmLibClasses entries
2026-09-08 06:32:06 -04:00
Brian Neumann-Fopiano 3551614088 Extend VolmLib storage hunks
- Make StorageHunk, ArrayHunk, AtomicHunk, SynchronizedArrayHunk, AtomicDoubleHunk, AtomicIntegerHunk and AtomicLongHunk extend their VolmLib counterparts and only bind the Iris Hunk interface
- Drop the Lombok data, equals and hashCode generation the duplicated storage hunks carried
- Ship the VolmLib storage hunks by removing their unusedVolmLibClasses entries
2026-09-08 06:32:06 -04:00
Brian Neumann-Fopiano 355d7f4132 Delete dead util duplicates
- Delete unused board BoardSettings and ScoreDirection wrappers around the VolmLib board types
- Delete unused inventorygui UIStaticDecorator, Window and WindowResolution wrappers around the VolmLib inventorygui types
- Delete unused VectorMath subclass of the VolmLib VectorMath
- Delete unused InterpolationType enum and the IrisInterpolation blerp overload that keyed on it
- Delete HunkFace and point Hunk.viewFace and Hunk.cropFace at the VolmLib HunkFace
- Drop the iris board and inventorygui excludes from supersededVolmLibPackages and the VectorMath entry from unusedVolmLibClasses
- Drop the deleted inventorygui files from the Bukkit purity allowlist
2026-09-08 06:32:06 -04:00
Brian Neumann-Fopiano 00474252b4 Drop stale artifact entry
- Remove VectorNoiseKernels2D from requiredBukkitArtifactEntries; the class was deleted with the unwired 2D kernels and verifyBukkitArtifact failed on every build
2026-09-08 06:32:06 -04:00
Brian Neumann-Fopiano cab35996f8 Fix identity digest separator
- Separate hashed studio project entries with a space instead of a null byte
- Repin the scripted session digest for the corrected separator
2026-09-08 06:20:54 -04:00
Brian Neumann-Fopiano 2451c89cee Split jigsaw studio service
- Move world registration and shutdown quiescing into JigsawStudioRegistry
- Move save, export and close arbitration into JigsawStudioSaveLifecycle
- Move autosave scheduling, drains and undo into JigsawStudioAutosaveScheduler
- Move variant creation, duplication and deletion into JigsawStudioVariantEditor
- Move variant metadata and resize edits into JigsawStudioVariantProperties
- Move workcell capacity, labels and connector blocks into JigsawStudioWorkcellEditor
- Move graph mutation and live relayout into JigsawStudioGraphMutations
- Move dynamic graph evaluation into JigsawStudioEvaluator
- Move menu state, studio tools and deferred duplication into JigsawStudioToolbelt
- Move the studio world event handlers into JigsawStudioProtectionListener
- Move ownership and mutation guards into JigsawStudioProtection
- Move variant materialization and hydration into JigsawStudioMaterializer
- Move dirty marking into JigsawStudioDirtyTracker
- Move workcell capture scheduling into JigsawStudioCaptureScheduler
- Move chunk capture and connector aggregation into JigsawStudioCapture
- Move materialized and rendered chunk writing into JigsawStudioChunkWriter
- Move jigsaw marker NBT watching into JigsawStudioTileWatcher
- Move particle preview rendering into JigsawStudioVisualization
- Move player workcell context into JigsawStudioPlayerContext
- Point the studio service tests at the relocated collaborators
- Record the split studio sources in the Bukkit purity allowlist
2026-09-08 06:20:54 -04:00
Brian Neumann-Fopiano 8b9d1fb2fb Split jigsaw studio menus
- Move each jigsaw menu kind into its own renderer class
- Move workcell capacity staging into JigsawStudioWorkcellMenu
- Move menu presentation helpers and adjustment math into JigsawStudioMenuFormat
- Promote the menu action contract to JigsawStudioMenuActions
- Point the menu controller test at the relocated menu types
2026-09-08 06:20:54 -04:00
Brian Neumann-Fopiano e7c6b6e08e Split jigsaw graph editor
- Move planar object resize geometry into JigsawStudioObjectResizer
- Move owned graph JSON document transforms into JigsawStudioGraphDocuments
- Move owned graph reference checks into JigsawStudioGraphInspector
- Point the graph editor resize test at the relocated resizer
- Add a scripted studio persistence session digest test over project and history bytes
2026-09-08 06:20:54 -04:00
Brian Neumann-Fopiano a4e22006e0 Split jigsaw command tree
- Move each jigsaw subgroup executor into its own top-level command class
- Move the adoption plan store, handlers and helpers into CommandJigsawAdopt
- Move the vanilla export lease and reporting helpers into CommandJigsawExports
- Move the studio open lifecycle into CommandJigsawOpen
- Point the jigsaw contract test at the relocated command types
- Exclude the new jigsaw command sources from the generation revision scope
2026-09-08 06:20:54 -04:00
Brian Neumann-Fopiano 85e12caf9b Split hydrology footprint and cave planners
- Move segment rasterisation and channel shaping out of HydrologyFootprintCompiler into HydrologyFootprintRasterizer and HydrologyChannelGeometry
- Move chamber, throat and pool construction out of HydrologyCaveContainmentPlanner into HydrologyCaveChamberPlanner
- Promote the candidate span, candidate builder, stencil, column and cave observation types out of the three compilers to top level
2026-09-08 05:57:18 -04:00
Brian Neumann-Fopiano 2fb1dc4fd6 Restore datapack bukkit purity
- Return the Minecraft version lookups to DatapackIngestService so no new file couples to org.bukkit
- Drop the redundant self imports of the service's own nested types
2026-09-08 05:57:18 -04:00
Brian Neumann-Fopiano a31d494f45 Split hydrology tile planner
- Keep planner state, caches, cross-tile entry points and terrain sampling in HydrologyPlanner and move tile planning into ten collaborators for cross-tile resolution, outlets, sources, route geometry, route paths, surface courses, tributaries, underground courses, hydraulic segments and feature sites
- Promote the shared planning records to top level so the collaborators address routes, grids, drafts and course paths directly
- Pin a full four-tile plan digest for two seeds in HydrologyPlanHashTest
- Requalify the reflective and package-private planner call sites in the hydrology tests onto the new owners
2026-09-08 05:57:18 -04:00
Brian Neumann-Fopiano 422618282e Split datapack ingest service
- Keep ingest orchestration and the public entry points in DatapackIngestService and move startup validation, staging reapply, removal, install planning, ownership, manifests, transactions and recovery into dedicated package classes
- Promote the shared ingest data types to top level so every collaborator addresses them directly
- Report the managed metadata IO failures on the reapply path instead of logging them at debug
- Requalify DatapackIngestServiceTest call sites onto the new owners without changing any test
2026-09-08 05:57:18 -04:00
Brian Neumann-Fopiano 9775a50b26 Split schema property builder
- Move registry catalogs, enum definitions and block-state groups out of SchemaBuilder into SchemaRegistryCatalog, SchemaEnums and SchemaBlockStates
- Decompose buildProperty into SchemaPropertyBuilder per-type handlers with SchemaRegistryProperties owning the per-annotation registry chains
- Report the font enumeration, registry list function and default value probe failures instead of swallowing them
- Pin the generated studio schema digest for fifteen pack roots in SchemaBuilderIdentityTest
2026-09-08 05:57:18 -04:00
Brian Neumann-Fopiano 290412b62a Cover untested contract surfaces
- Add a test source set to spi with bind, service registry and platform value contracts
- Prove the DataType probe contract, the ExternalDataProvider defaults and the string-only provider namespace claims
- Prove Identifier parsing and rendering
- Cover the data version, primitive and vector director handlers
- Cover the vanilla export identifier, block state, orientation, request and settings values
- Cover the structure adoption limits, diagnostics and result invariants
- Cover the pregen mantle backpressure, heap reclaim urgency, empty cache, listener defaults and diagnostic descriptions
2026-09-08 05:27:49 -04:00
Brian Neumann-Fopiano 71e3af727f Parameterize template-identical tests
- Split PackValidatorCustomBiomeSpawnTest into parameterized accepted and rejected suites plus the resolver-failure case
- Drive IrisCarveWallPaletteScopeTest from its four generation-scope modes
- Parameterize the legacy bilerp parity samples in IrisComplexGridBoundsCacheTest behind Enclosed
- Parameterize the resolved bounds in IrisStructureYBandTest
- Parameterize the entity key normalization in IrisBiomeCustomSpawnTest
- Parameterize the platform identity in IrisWorldIdentityTest
- Parameterize the layer lock in IrisBiomeLayerSlopeSourceTest
2026-09-08 05:27:49 -04:00
Brian Neumann-Fopiano c3bc0f5d18 Split the router fork floor
- Replace GenerationHistoryRuntimeRouterTest with promotion, routing and lifecycle classes
- Add GenerationHistoryRuntimeRouterSupport on top of GenerationHistorySupport for the shared fakes and helpers
- Drop the router-local copies of createPack, createHistory, stage, contract, signature and writeRegion
2026-09-08 05:27:49 -04:00
Brian Neumann-Fopiano 9dcaed5315 Share test fixtures across modules
- Move art.arcane.iris.testsupport into core/src/testFixtures and apply java-test-fixtures to core
- Consume core test fixtures from the bukkit plugin, nms, probe and the three loader adapters
- Delete the duplicate ProjectPaths copies in the bukkit-plugin and modded-common test trees
- Point ModdedMessageContributorTest at the shared ProjectPaths
- Guard CommandPackCompatTest, NativeTransitionColumnTest and StubPlatformStateTest with PlatformLeakGuard
- Release the probe runtime state after DimensionStackSuperflatProbeTest opens a workspace
- Keep core main classes ahead of the core jar on the test classpath so the bytecode gates still scan a directory
2026-09-08 05:27:49 -04:00
Brian Neumann-Fopiano fd146de1ad Drop added code comments
- Remove the Javadoc and inline comment blocks the three runtime-safety commits added under src/main
- Keep the GPL file header on the new spi type, matching every other file in that module
- Leave the superseded stale-temp ordering comment deleted rather than restoring text the new boot path contradicts
2026-09-08 04:48:43 -04:00
Brian Neumann-Fopiano 486c6eeae9 Defer runtime injection
- Add RuntimeInjection so the Java agent attach and the server-code injection run once, on demand, instead of during every enable
- Install the injection when a world Iris created is about to be built and when Bukkit asks for the generator of a world configured outside Iris
- Lock the runtime with the same reason a boot-time injection failure used to raise when the deferred install fails
- Add general.eagerRuntimeInjection to keep boot-time injection and its startup verification
- Re-read the startup lock after a deferred install so a failed install refuses the world instead of generating it
- Move the stale cache/temp sweep off the boot thread and make every pack import wait it out
- Move the default-pack presence warning off the boot thread; it walks the whole pack tree to pick a warning
- Build the wand and dust templates on the first tick instead of during service enable
- Report enable duration, per-phase timings and the three slowest services on every boot
- Drop the wand and dust templates on service disable
2026-09-08 04:45:55 -04:00
Brian Neumann-Fopiano 6c6dba0232 Release runtime statics
- Null Iris.instance, the console sender, the Multiverse link, the compat set and the chunk tickets at the end of teardownRuntime
- Bind the host bridge suppliers to the constructing plugin instead of reading the instance static on every call
- State that Iris is disabled when service, getTemp or getSender is reached after teardown instead of raising a bare NullPointerException
- Add IrisRuntimeStatics so a disable drops the service registry, both capability singletons, the loaded datapack runtime and the localization roots
- Add WorldLifecycleService.reset and WorldRuntimeControlService.reset so the next boot snapshots the server it is running on
- Add ServerConfigurator.resetLoadedDatapackRuntime so an enable revalidates instead of trusting the previous boot's fingerprint
- Clear the localization data folder and plain-text memo on shutdown
- Add BukkitPlatform.releaseHost and call it last in terminal cleanup so the disabled plugin, console sender and log bridge stop being reachable from a static
2026-09-08 04:44:32 -04:00
Brian Neumann-Fopiano f83cf71b11 Report swallowed failures
- Add art.arcane.iris.spi.CapabilityProbe so an absent optional API is recorded at debug with the failure class and message instead of a bare ignored catch
- Route runtime day-time reads, clock pushes and the noon time lock through IrisLogging.reportError with the world name and the full trace
- Report a level storage access that will not close, so a retained world session lock names its cause
- Report a failed iris.json rewrite with the file path instead of dropping the IOException
- Report Iris world registry and dimension read failures during Multiverse reconciliation instead of logging one debug line
- Report a failed pack compatibility summary and a failed vanilla-slot capture with their traces
- Report a superseded generation semantics shard that cannot be deleted, once, with its path
- Restore the interrupt flag when the global cache trimmer join is interrupted
- Replace ValueWithDiagnostics with CheckResult so a startup check carries the runtime lock reason it raises
- Give every startup check an explicit advisory or critical severity and lock the runtime on any Danger result, replacing the catch that recognised the injection check by id
- Lock the runtime when general.disableNMS holds a no-op binding or the NMS binding is unbound, instead of marking the runtime ready
- Match incompatible plugin names against every registered plugin without regard to case
- Name the JVM maximum heap in the memory check output
- Print the lock reason and what it blocks when Iris enables into a locked runtime
- Carry the per-check safeguard modes and diagnostic lines in /iris debugdump
2026-09-08 04:44:31 -04:00
Brian Neumann-Fopiano 45b9ff8e11 Drop unwired 2D noise kernels
- Delete NoiseKernels2D, ScalarNoiseKernels2D and VectorNoiseKernels2D after measuring the vector kernel at 0.07x scalar on 2-lane NEON with no production call site
- Delete the 2D noise parity test and the SimdSupport plumbing that selected between the two kernel sets
- State in the simd-bench README that its 2D noise classes are a candidate Iris does not ship
2026-09-08 04:33:32 -04:00
Brian Neumann-Fopiano 79af11c435 Trim generation hot path allocations
- Replace the six per-voxel IrisPosition probes in MantleWriter neighborhood hollowing with an origin-relative primitive key set
- Look mask membership up through the same primitive key set instead of allocating an IrisPosition per candidate voxel
- Give the squared-distance helper a fixed arity so ballooning no longer depends on escape analysis to drop its varargs array
- Resolve each Matter slice once per write instead of paying a hasSlice probe and a getSlice lookup for the same class
- Read the river water surface through the primitive stream accessor instead of boxing a Double per column
- Add nullable primary surface layer accessors so the terrain actuator stops building two Optionals per column
- Add MantleWriterNeighborhoodTest, MantleWriterNeighborhoodBenchmarkTest and HydrologyColumnSampleAccessorTest
2026-09-08 04:33:32 -04:00
Brian Neumann-Fopiano 1be563bd28 Unblock pregen throughput limiters
- Replace the pregen cache global monitor with atomic per-plate and per-region state and a lock-free resident-plate memo
- Serialize plate eviction and its disk write under the residency lock so a reloaded plate can never lose a concurrent bit
- Retry a chunk or region cache write whose plate was retired mid-write
- Delete SynchronizedCache and the isThreadSafe wrapper it existed for
- Replace the AsyncPregenMethod semaphore and permit monitor with PregenAdmissionGate, one condition that signals on release, adaptive-limit change and cancellation
- Attribute the adaptive and permit waits separately from the single gate wait
- Wait on a progress signal instead of sleeping 25ms in PregenMantleBackpressure, signalled by chunk completion
- Wait on a state signal instead of sleeping 50ms and 150ms in IrisPregenerator pause, heap pressure and tectonic reclaim loops
- Restore the burst pool parallelism when the last pregen job finishes
- Add PregenCacheConcurrencyTest, PregenAdmissionGateTest and PregenWaitPolicyContractTest
2026-09-08 04:33:31 -04:00
Brian Neumann-Fopiano e030ac3be5 Report silent pregen failures
- Add PregenDiagnostics.probeFailed so capability probes log their name, class and message at debug instead of vanishing
- Report the moonrise worker pool, mantle radius, engine access and Folia submit probe failures through that helper
- Report async pregen mantle cleanup failures through IrisLogging.reportError with the trace instead of dropping them
- Report a stranded plugin chunk ticket through IrisLogging.reportError so the leaked pin is visible
- Emit the chunk load failure trace before the metrics snapshot so a failing snapshot can no longer silence it
- Report the pregen cancellation and mantle handle probe failures in PregenMantleBackpressure
- Report the Medieval engine resolution probe failure
- Record the real Vector API initialization failure in SimdSupport and print the accurate kernel selection reason
- Add PregenDiagnosticsTest, PregenSilentFailureContractTest and SimdSupportStatusTest
2026-09-08 04:33:31 -04:00
Brian Neumann-Fopiano 9d22a18a3a Close route off the caller thread
- Complete the async route close on the worker executor so the cross-thread assertion no longer depends on whether the first stage finished before the handler was attached
2026-09-08 04:28:40 -04:00
Brian Neumann-Fopiano e5b7cb97a8 Anchor repository root probe
- Require the core source tree alongside the settings file when walking up for the repository root, so modded-common tests built inside a loader adapter no longer stop at that adapter's own settings file
2026-09-08 04:28:40 -04:00
Brian Neumann-Fopiano bdb13d1647 Fix cross-thread report assertion
- Assert the spawn failure report through the bound platform instead of a thread-confined static mock, so the report is seen whichever thread wins the reporting race
- Guard WorldEntitySpawnerLifecycleTest against a leaked platform binding
- Unbind the stub platform after the probe module's platform state test
2026-09-08 04:28:17 -04:00
Brian Neumann-Fopiano 42798c6340 Poll instead of sleeping
- Add the Await test helper that polls a condition every ten milliseconds under a bounded timeout
- Replace every Thread.sleep in the fourteen polling test classes with a condition wait, a latch wait or a bounded quiet window
- Keep the crash-consistency fsync interception in GenerationHistoryAdmissionTest and SavedBiomeStoreBatchTest on real barriers
2026-09-08 04:28:17 -04:00
Brian Neumann-Fopiano c75b0e01af Resolve test paths from code source
- Add the ProjectPaths test helper that derives the module root from the test class code source and the repository root from the settings file above it
- Replace the working-directory-relative source and resource literals in thirty-five test classes with module and repository lookups
- Drop the bespoke source-root walk in PregenMantleCleanupContractTest
2026-09-08 04:26:33 -04:00
Brian Neumann-Fopiano 03c645b66f Isolate platform binding
- Add the PlatformBinding rule that binds a mock platform for a class or method, unbinds on teardown and restores the previous binding
- Add the PlatformLeakGuard rule that fails a class when a platform is still bound or a service is still registered at class start or end
- Add IrisRuntimeState so the guard reads the registered services without adding a test-only method to the SPI
- Replace the inline mock-platform setup and teardown of twenty test classes with the rule
- Guard the remaining sixty-four platform and service touching core test classes
- Rename the pack-compat pool fixtures to PackCompatFixtures so the two CompatFixtures names cannot collide
- Scope BukkitTestServer branding to a ServerBranding rule that restores the shared mock after each test
2026-09-08 04:22:49 -04:00
Brian Neumann-Fopiano c332c86849 Relax test fsync
- Add Durability, a process-wide fsync policy defaulting to real barriers and switchable to relaxed through the iris.durability system property
- Route the history, studio, journal, structure and datapack fsync barriers through Durability so relaxed mode skips both the force and the channel open
- Add the DurabilityMode test rule and enable relaxed durability on the twenty slowest core stores tests
- Split GenerationHistoryTest into epoch, legacy adoption and recovery classes over a shared GenerationHistorySupport base
2026-09-08 04:18:01 -04:00
Brian Neumann-Fopiano c75cfd387e Retire source-text tests
- Delete 22 test classes whose assertions only read production .java text and check call ordering or literal presence
- Remove 77 source-text assertion methods from 34 test classes that keep real behavioural coverage
- Replace the core process-stream source grep with an ASM gate over compiled core classes in CoreProcessStreamPurityGateTest
- Replace the packs-root source grep in PacksFolderContractTest with an ASM call-site gate over compiled core classes
- Replace the IrisData registered-root regex in PackTypeBukkitPurityGateTest with an ASM scan of the registerLoader class constants
- Trim six mixed tests down to their behavioural assertions and drop their source-text tails
- Drop the eight iris.*Source and three iris.moddedCommonSources test system properties now that nothing reads them
2026-09-08 04:01:33 -04:00
Brian Neumann-Fopiano 7841efed54 Run modded and buildSrc tests
- Add a root moddedTest that runs adapters/modded-common through the Fabric build
- Make check depend on moddedTest, ordered ahead of the loader jars
- Gate the buildSrc jar on the buildSrc tests so they stop being skipped
- Drop an unreachable classpath guard from SchemaBuilderPlatformIsolationTest
2026-09-08 03:41:15 -04:00
Brian Neumann-Fopiano 89dc5d0447 Drop execution-time project access
- Delete stale jars through an injected FileSystemOperations in the root build
- Delete stale loader jars through an injected FileSystemOperations in each adapter
- Resolve stale-jar file trees and copy sources at configuration time
- Expand the loader metadata templates from the configured version string
2026-09-08 03:41:03 -04:00
Brian Neumann-Fopiano 5c84c6b8d8 Derive modded bukkit baseline
- Read the modded bukkit-supertype baseline from core/purity-allowlist.txt
- Match nested classes against their top-level source entry
- Exempt VolmLib classes, which Iris' core ratchet does not govern
- Delete the hand-frozen knownBukkitSupertypeClasses inventory
2026-09-08 03:40:51 -04:00
Brian Neumann-Fopiano 0bdacc6fed Restore modded jar incrementality
- Drop outputs.upToDateWhen and outputs.cacheIf from the loader jar Exec tasks
- Declare lombok.config, core/agent and both revision scripts as modded inputs
2026-09-08 03:40:40 -04:00
Brian Neumann-Fopiano 6e3e1f3c64 Single relocation declaration
- Add gradle/runtime-relocations.gradle as the one runtime-download relocation map
- Read the slimJar relocations and shrinkRelocations from that map
- Pass the derived relocation prefixes into BukkitArtifactVerifier
- Drop the duplicate jar size check the packaging audit already enforces
2026-09-08 03:40:29 -04:00
Brian Neumann-Fopiano c83d6964f1 Gate mavenLocal behind property
- Stop mavenLocal shadowing every remote repository in allprojects
- Add -PuseMavenLocal, on by default only when the composite VolmLib is disabled
2026-09-08 03:40:15 -04:00
Brian Neumann-Fopiano b225c0b5be Ratchet net.minecraft purity
- Add minecraftPurityRatchet over core/minecraft-purity-allowlist.txt, wired into check
- Register both purity ratchets from one closure instead of duplicating the scan
- Prune WorldCreationProgressReporter from core/purity-allowlist.txt
2026-09-08 03:39:43 -04:00
Brian Neumann-Fopiano 1913baed17 Baseline Oraxen provider
- Add the Bukkit-only Oraxen data provider to the known Bukkit-supertype set so the modded artifact verifiers accept it like the ItemsAdder provider
2026-09-08 03:02:23 -04:00
Brian Neumann-Fopiano 031d4ade2c Wire Iris jar shrink
- Wire the ProGuard shrink pass into the Bukkit artifact with the core, Bukkit plugin and NMS compile classpaths as library jars
- Map the fourteen SlimJar relocations so runtime-downloaded libraries resolve under their relocated names
- Add packaging/shrink.pro keeping the public API, Iris events, platform SPI, VolmLib integration contract and Bukkit event handler lists
- Lower maximumBukkitArtifactBytes to 7,600,000 so verifyBukkitArtifact enforces the same budget as the packaging gate
- Build VolmitSender.name() from the sender name instead of the relocated CommandSender.name() reference, which cannot resolve on Paper 26.2
2026-09-07 23:28:48 -04:00
Brian Neumann-Fopiano 9b9ca8de34 Unlatch platform block states
- Replaced the nine static final PlatformBlockState fields in IrisDimensionStackActuator, IrisTerrainNormalActuator, IrisPerfectionModifier, IrisPostModifier, IrisBiomeLayerGenerator, BiomeBuffetGenerator, ChunkDataHunkHolder and EngineMantle with BoundBlockState holders that re-resolve when a different platform binds, costing one volatile read of the cached binding, one of the bound platform and a reference compare per read
- Turned the IrisPerfectionModifier ore-hiding table into a Map of BoundBlockState so the four base states follow the bound platform instead of the first one loaded
- Kept the last resolution in BoundBlockState while nothing is bound so in-flight generation on an unbinding server reads the same states it did before, matching what B.registries already does
- Added IrisPlatforms.getOrNull so the block-state holders read the binding once without an exception on a bare JVM
- Added PlatformStateRebindTest, which fails on the old code: it binds two mock platforms in turn and asserts ChunkDataHunkHolder and EngineMantle serve the second one, and rejects any static PlatformBlockState field left on the converted classes
- Indexed the .iob write palette by key so palette lookup is a hash probe rather than a scan of the palette list, cutting the 32767-state IrisObjectIoWriteLimitsTest from 4.9s to 0.7s while writing byte-identical files
- Built the palette once per write instead of once in validateWritable and again in the writer, and shared it between the plain and progress-reporting write paths
- Added IrisObjectIoPaletteTest, which pins the SHA-256 of a 30000-block, 3011-state object written by the previous implementation and asserts the palette keeps first-seen insertion order
- Added BukkitTestServer, a single mock Bukkit server installed once per JVM under a lock, stubbing logger, name, version, plugin manager and both createBlockData overloads, and failing loudly if the install does not take
- Moved IrisRuntimeSchedulerModeRoutingTest, WorldRuntimeControlServiceTimeLockTest, PreObjectMatterTest, CNGInjectorParityTest, BukkitSpiConformanceTest, MantleWriterPreObjectJournalTest, IrisFloatingChildBiomesCarvingResolutionTest and IrisDimensionCarvingResolverParityTest onto it, so whichever class runs first no longer decides whether createBlockData or getPluginManager is stubbed
- Extracted the KeyedBlockState test double out of IrisObjectIoWriteLimitsTest so the palette test can reuse it
- Replaced the IrisServices.clear() teardowns in nine test classes with a targeted remove of the PreservationRegistry each one registers, so a teardown no longer wipes a binding it did not install
- Registered a PreservationRegistry in PackDownloaderTest and StudioSVCWorldPackPublishTest, which reach IrisData.get and openRuntime and were silently getting loaderless IrisData whenever no other class had leaked one
2026-09-07 19:59:22 -04:00
Brian Neumann-Fopiano 266fe0d71c Faster green test suite
- Removed the core processResources skip that fired on test-only invocations, which packaged a core jar with no generation kernel catalog and made all 10 probe and bukkit-plugin tests that reach GenerationKernelRegistry fail on every ./gradlew test
- Set maxParallelForks on :core:test to half the available processors, overridable with -PtestForks, and left the small modules single-forked so they do not re-pay the Minecraft bootstrap per fork or steal worker leases from core
- Resolved B.registries() per bound platform instead of latching the first one for the JVM lifetime, keeping the previous unbound behaviour, so a test that binds its own platform no longer reads a foreign platform's block states
- Resolved IrisObject.States per bound platform through accessors instead of static finals fixed at class-init, removing the NoClassDefFoundError and null material-key failures that appeared whenever class partitioning changed
- Bound an explicit platform in EngineMantleCoverageTest and IrisDimensionStackActuatorMetadataTest so they no longer depend on another class leaking a binding first
- Gated BukkitSpiConformanceTest registry probes on a live RegistryAccess and restored the IrisPlatforms binding it leaked, which was silently swallowing the stderr two SettingsHotloadWatchTest assertions read
- Initialised org.bukkit.Registry and Biome inside the RegistryAccess mock scope in ObjectStudioGeometryTest so Mockito no longer instruments Registry mid-clinit
- Replaced the SettingsHotloadWatchTest fixed 1200ms diagnostic window with a 10ms condition poll bounded at 4s
- Replaced the IrisCaveCarver3DNearParityTest wall-clock speedup benchmark with a deterministic density-sample count, cutting 28 carves to 2
- Cut HydrologyPlannerTest cold-owner parity from 12 tile plans to 4 and shared the immutable baseline plan across the early-owner tests
- Built one prototype project or staged history per class in the jigsaw studio and world replacement tests and copied it per test, replacing repeated fsync-bound fixture construction
- Replaced Mockito block-state fixtures with concrete implementations in the object IO, deposit modifier and nearest-block-index tests, removing about 2.5 million interceptions
- Replaced Thread.sleep with bounded condition polling in HydrologyTileCacheTest
- Deleted 19 test classes and 60 further test methods that asserted on production .java source text rather than behaviour, and removed the 19 now-unread iris.*Source system properties that fed them
2026-09-07 19:59:22 -04:00
Brian Neumann-Fopiano d3245aa1c2 IA Fixes
- Thsi fixes an integration between Itemsadder and Oraxen.
- Waiting on oraxen folks
2026-09-07 19:22:59 -04:00
Brian Neumann-Fopiano 894cef73a6 Studio fixes.
- Fixes the Object and Jigsaw studio errors from the coretapper i made.
2026-09-07 15:23:25 -04:00
Brian Neumann-Fopiano 0dcd1900be Improved world generation
- Terrain and cave scans skip repeated work
- River and structure lookups reuse cached results
- Chunk history saves share disk writes
- History lookups continue during disk writes
- Failed saves retain their original errors
- Deposit failures stop incomplete chunks
- Tests check output and save safety
2026-09-07 03:17:09 -04:00
Brian Neumann-Fopiano 151677f92f Improved pregen persistence
- Saved biome records use one durable append with shorter lock ownership
- Terrain receipts buffer field writes before compression
- Native cache retirement follows the current index
- Server shutdown drains native chunk tasks before world close
- Shutdown flushes retain native save ownership
- Regression coverage checks persistence and shutdown races
2026-09-06 18:27:51 -04:00
Brian Neumann-Fopiano 083e689864 Drained pregen before shutdown
- Server shutdown advances native chunk tasks while waiting for pregeneration
- Native world close owns chunk saves during server shutdown
- Pending plugin flushes stop waiting when native shutdown begins
- Late eviction completion cannot schedule another shutdown flush
- Regression tests cover owner waits and shutdown save ownership
2026-09-06 18:20:26 -04:00
Brian Neumann-Fopiano 319f8aed47 Added native chunk draining
- Native chunk queues can advance on their owning server thread
- Folia and foreign threads cannot run global chunk promotion
2026-09-06 18:13:57 -04:00
Brian Neumann-Fopiano 0324b0c467 Added native chunk pumping
- Add target-world native chunk task polling
- Guard server lifecycle ownership and skip Folia polling
- Cover native queue delegation and ownership guards
2026-09-06 18:13:36 -04:00
Brian Neumann-Fopiano 88008a7fd2 Corrected native cache retirement
- Invalidated volume indexes receive runtime retirement callbacks
- Retired builds cannot republish results after cache eviction
2026-09-06 17:55:45 -04:00
Brian Neumann-Fopiano c8fa5d9d8b Fixed native cache retirement
- Transferred retirement listeners when replacing native structure indexes.
- Prevented retired in-flight builds from repopulating origin and query caches.
- Added deterministic invalidation and eviction regression tests.
2026-09-06 17:55:25 -04:00
Brian Neumann-Fopiano bbf14a7967 Reduced biome persistence overhead
- Biome encoding runs outside the region write lock
- Appends use one framed write before durable publication
- Concurrent claims preserve conflict checks and rollback behavior
2026-09-06 17:44:38 -04:00
Brian Neumann-Fopiano b4440463a5 Reduced saved biome writes
- Saved biome encoding runs outside region locks
- Claims recheck stored content before appending
- Record framing uses one write with unchanged bytes
- Tests cover durable publication, rollback and concurrent pruning
2026-09-06 17:43:28 -04:00
Brian Neumann-Fopiano 04103bd15a Fixed pregen shutdown scheduling
- Scheduler admission avoids the plugin manager monitor
- Saving callbacks no longer block the joined pregenerator during disable
2026-09-06 17:41:40 -04:00
Brian Neumann-Fopiano bb967a673e Buffered terrain receipt encoding
- Native terrain receipts batch compressor input
- Receipt format and geometry remain unchanged
2026-09-06 17:41:40 -04:00
Brian Neumann-Fopiano 634e6ffe57 Buffered native terrain receipts
- Terrain fields reach the compressor in buffered batches
- Receipt bytes and decoded natural geometry retain their current format
- Binary contract coverage includes complete and boundary columns
2026-09-06 17:41:38 -04:00
Brian Neumann-Fopiano be621c0074 Fixed pregenerator shutdown lock
- Scheduler admission reads the hosted plugin state without taking the plugin manager monitor
- Shutdown regression joins a worker already dispatching a saving event
- Event tests preserve normal delivery and full failure reporting during scheduler rejection
2026-09-06 17:37:48 -04:00
Brian Neumann-Fopiano 2415825bb7 Unblocked generation stage reads
- Generation stages read immutable metadata without waiting for semantic journal flushes
- Durable claims and activation cutovers retain their synchronization
- Regression tests cover admission contention, publication ordering, and failure cleanup
2026-09-06 16:31:02 -04:00
Brian Neumann-Fopiano 364d127658 Unblocked generation metadata reads
- Opened generation stages independently of semantic journal flushes
- Preserved durable claim publication and cutover ordering
- Added concurrency and failure regression coverage
2026-09-06 16:24:12 -04:00
Brian Neumann-Fopiano 48a81a3d2c Unblocked history stage admission
- Generation stages read coherent metadata from the immutable store manifest
- Stage admission proceeds while unrelated semantic journal flushes hold the history monitor
- Concurrency tests preserve durable publication, cutover ordering, and failed admission cleanup
2026-09-06 16:22:24 -04:00
Brian Neumann-Fopiano 763f9df2c2 Refreshed pregeneration runtime
- Generation router reads avoid the attachment monitor after publication
- Desktop pregeneration uses a proportional map with distinct controls and metrics
- Preview updates remain bounded through large targets and window state changes
- Pause controls stay bound to their owning job
- Startup failures retain a readable error window
- Bundled language catalogs cover the revised window
2026-09-06 14:48:37 -04:00
Brian Neumann-Fopiano 21d6c2c9f9 Corrected pending time estimates
- Preview shows Pending while remaining chunks have no time estimate
2026-09-06 14:43:22 -04:00
Brian Neumann-Fopiano 28c715ff51 Redesigned pregeneration map
- Desktop pregeneration uses a proportional map with labeled controls and metrics
- Pixel updates remain bounded across large targets and minimized windows
- Preview lifecycle keeps pause controls attached to their owning job
- Bundled language catalogs include the revised desktop labels
2026-09-06 14:36:32 -04:00
Brian Neumann-Fopiano e2761af959 Redesigned pregeneration map
- Native window separates map content from progress and resource status
- Pause controls stay bound to their job through shutdown and replacement
- Inclusive map bounds and fixed raster buffers preserve edge updates without idle repaints
- Minimized windows retain bounded status updates while skipping terrain reads
- Startup failures retain a readable error view after background work stops
- All bundled locales include the new controls and metric labels
- Regression tests cover map edges, coalescing, snapshots, and control ownership
2026-09-06 14:36:09 -04:00
Brian Neumann-Fopiano 95323150c0 Reduced coordinate routing contention
- Published router reads bypass the attachment monitor

- Detached routing retains synchronized validation

- Concurrent reads retain coordinate and lifecycle checks
2026-09-06 14:21:54 -04:00
Brian Neumann-Fopiano eb43cc44fa Reduced coordinate routing contention
- Published history router reads bypass the attachment monitor
- Missing router reads retain synchronized attachment and detach checks
- Concurrent tests cover published reads and pending lifecycle changes
2026-09-06 14:21:07 -04:00
Brian Neumann-Fopiano bfaacb4d38 Improved generation runtime stability
- Generation admission reduces contention while preserving cutover priority
- Native POI changes survive generation during server shutdown
- Chunk ownership and retained POI tickets are covered by regression tests
2026-09-06 13:52:07 -04:00
Brian Neumann-Fopiano af42c69824 Preserved native POI ownership
- Native structure placement records pending POI repairs before block changes
- Chunk loads reconcile affected sections on their owning thread
- Matching POI records retain occupied tickets across repair
- Native capability checks preserve supported version behavior
2026-09-06 13:34:50 -04:00
Brian Neumann-Fopiano a28614c04e Fixed native POI ownership
- Native structure POI changes persist dirty chunk sections before deferred updates
- Chunk load reconciles final POI states on the owning thread
- Matching POI records retain claimed tickets
- Native flag detection keeps unsupported server behavior intact
- Regression tests cover persistence and failed repair retries
2026-09-06 13:34:30 -04:00
Brian Neumann-Fopiano 0d2e8a45ff Reduced admission lock contention
- Stage admission avoids FIFO metadata lock handoffs
- Cutover priority and runtime retirement retain their existing checks
2026-09-06 13:16:16 -04:00
Brian Neumann-Fopiano 34c2e4e560 Reduced admission lock contention
- Generation admission uses nonfair metadata lock handoffs
- Waiting cutovers retain priority over later generation stages
- Concurrent stage tests verify cutover drain and runtime retirement
2026-09-06 13:07:52 -04:00
Brian Neumann-Fopiano f3b6a0ec1b Fixed pregen runtime bottlenecks
- Generation routing reduces metadata lock contention while retaining lifecycle ownership
- Native biome wrappers avoid repeated reflection and key allocation
- Studio scoreboards recover from pending saved biome reads with cave overrides intact
- Pregeneration previews retain saved definitions on a bounded renderer worker
- Full build and runtime checks cover scoreboard recovery and Leaf pregeneration
2026-09-06 12:19:31 -04:00
Brian Neumann-Fopiano e5e3ae112d Fixed saved biome scoreboard
- Studio scoreboards recover while saved biome reads are pending
- Combined environment queries retain mantle cave overrides
- World changes refresh rows and genuine failures retain their cause
2026-09-06 12:12:15 -04:00
Brian Neumann-Fopiano 9fdb5615d6 Bounded routing test cleanup
- Shutdown assertions interrupt and drain workers before fallback router close
2026-09-06 12:11:56 -04:00
Brian Neumann-Fopiano 3425c05e8b Fixed saved biome scoreboards
- Keep Studio board refreshes alive while saved biome data loads
- Sample mantle-aware biome environments and clear prior world values
- Report permanent failures once per display context
- Localize loading states and verify recovery and Jigsaw transitions
2026-09-06 12:11:41 -04:00
Brian Neumann-Fopiano fb4585b62d Reduced generation routing contention
- Ready runtime leases avoid repeated metadata lock acquisition
- Concurrent routing retains shutdown and history ownership checks
2026-09-06 12:09:49 -04:00
Brian Neumann-Fopiano e69140e752 Cached native biome handles
- Terrain capture reuses bounded biome wrappers by native identity
- Canonical biome keys use the supported typed accessor
2026-09-06 12:09:49 -04:00
Brian Neumann-Fopiano 6b450fdea4 Fixed saved biome previews
- Pregeneration paints saved biome details on its bounded renderer worker
- Pending queries retain historical definitions through preview consumption
2026-09-06 12:09:49 -04:00
Brian Neumann-Fopiano fca4a58388 Reduced generation routing contention
- Ready generation bindings acquire their lease in one metadata lock pass
- Shared routing metadata uses nonfair lock handoffs under worker contention
- Single-runtime release avoids allocating an empty retirement list
- Concurrent route tests verify shutdown drains and rejects active workers
2026-09-06 12:09:33 -04:00
Brian Neumann-Fopiano cb85a09c0a Cached native biome handles
- Biome wrappers read canonical keys only on cache insertion
- Native handle identity prevents stale biome reuse after registry replacement
- The biome wrapper cache retains at most 4096 entries
- Regression tests cover key reuse, namespaces, replacement handles, and eviction
2026-09-06 12:08:31 -04:00
Brian Neumann-Fopiano df717cf151 Fixed saved biome previews
- Pregeneration previews wait for saved biome reads on the bounded renderer worker
- Preview colors retain historical definitions through rendering
- Existing chunk previews avoid biome reads while the window is hidden
- Query completion handles cache eviction and capture refresh
- Regression tests cover read failures, interruption, and shutdown ownership
2026-09-06 12:05:19 -04:00
Brian Neumann-Fopiano 5570923d30 Fixed saved world startup
- Check spawn chunks asynchronously before hydrology prefetch
- Reject stale runtime callbacks and retain failure stacktraces
- Verify saved-world Folia startup and Paper create unload reload
2026-09-06 05:17:22 -04:00
Brian Neumann-Fopiano d093ff0e41 Fixed Folia spawn initialization
- Check existing spawn chunks asynchronously during world initialization
- Guard delayed hydrology preparation with runtime lifecycle ownership
- Preserve prefetch failure reporting during concurrent shutdown
2026-09-06 05:08:32 -04:00
Brian Neumann-Fopiano 03b0f6e801 Fixed spawn prefetch startup
- Spawn hydrology checks existing chunks asynchronously without generating them
- Prefetch admission rejects closed or replaced generation runtimes
- Studio entry retains tracked hydrology preparation
- Admitted planning failures retain their stacktraces during shutdown
- Regression coverage checks startup admission and delayed completion
2026-09-06 05:08:13 -04:00
Brian Neumann-Fopiano 04471678b2 Refined Iris generation core
- Terrain, hydrology, and feature generation avoid repeated work and temporary allocations
- Deposits preserve configured ordering and independent chance samples
- Object transforms and layer boundaries handle previously failing cases
- Runtime shutdown, storage failures, and world reload retain resource ownership through completion
2026-09-06 04:52:13 -04:00
Brian Neumann-Fopiano f916c81bc0 Restored world reload lifecycle
- Generation admission retires after complete runtime cleanup
- Fresh world loads can activate pending compatible updates
- Failed generator closes remain sealed and retryable
2026-09-06 04:47:26 -04:00
Brian Neumann-Fopiano 2b124a2a25 Fixed world reopen admission
- Generation admission retires after every runtime closes
- Engine setup retains admission across activation replacements
- Failed generator closes stay sealed and retryable
- Regression tests cover reopening and pending promotions
2026-09-06 04:46:38 -04:00
Brian Neumann-Fopiano 62f6a32362 Reviewed object placement changes
- Object transform and smart boring changes remain integrated
2026-09-06 04:19:22 -04:00
Brian Neumann-Fopiano c49a9ddba8 Preserved complex hotload ownership
- Failed replacement cleanup remains owned for shutdown retry
- Published replacements remain current after late activation failure
2026-09-06 04:18:24 -04:00
Brian Neumann-Fopiano d454f25f4d Retained failed hotload complexes
- Retain unpublished complex assemblies when cleanup fails
- Keep published replacement runtimes owned after activation failure
- Cover failed cleanup retries and successful hotload rollback
2026-09-06 04:17:40 -04:00
Brian Neumann-Fopiano 723426177c Separated deposit chance samples
- Clump seeds mix the generator seed and attempt index before sampling
- Deposit plans share immutable seeds across preparation workers
- Probability checks cover generator independence and adjacent clumps across batch boundaries
2026-09-06 04:16:09 -04:00
Brian Neumann-Fopiano 0bfb4d6718 Completed shutdown retry ownership
- Retain generation stages and world services until shutdown completes
- Preserve failed runtime cleanup across resource hash changes
2026-09-06 04:09:34 -04:00
Brian Neumann-Fopiano 046b389925 Completed shutdown retry ownership
- Mark generation modes closed only after every stage finishes cleanup
- Retain manager loop and cleanup executor ownership until their existing shutdown waits succeed
- Propagate manager shutdown timeouts and interruption so resource release remains blocked
- Track unpublished runtimes by identity while resource state changes during cleanup
- Verify failed joins, interrupted waits, stage retries, and changing resource hashes
2026-09-06 04:07:51 -04:00
Brian Neumann-Fopiano fda24db789 Refined featurette edge behavior
- Keep coral tips attached to the fan silhouette
- Preserve large leaf weights and disable zero threshold ores
- Reuse formation palette sources across each object
2026-09-06 03:58:21 -04:00
Brian Neumann-Fopiano 2c66367606 Retained failed generation ownership
- Keep writers until cancelled components actually finish
- Retain incomplete runtime and target ownership for retries
- Gate storage release on completed service drains and saves
2026-09-06 03:58:02 -04:00
Brian Neumann-Fopiano 9a1c3546ba Preserved mantle task ownership
- Keep component cancellation separate from actual completion before releasing writers and deduplication claims
- Reject cancelled queued component claims before they can start writing
- Retain failed runtime assemblies and unpublished runtimes for shutdown retry
- Gate storage and data release on completed runtime cleanup
- Cover cancellation, shared writers, interrupted waits, and failed shutdown retries
2026-09-06 03:57:35 -04:00
Brian Neumann-Fopiano 0efe919ff4 Reused formation palette sources
- Formation resolution shares one base palette source per object
- Strata reuse one seeded source per occupied band
- Mixed palette tests preserve block choices across negative bands and reversed traversal
2026-09-06 03:56:40 -04:00
Brian Neumann-Fopiano ce431b524e Fixed featurette edge cases
- Fan coral tips follow the existing top of each occupied column
- Large secondary leaf weights preserve their authored proportions
- Zero ore thresholds skip noise sampling and block generation
- Regressions cover fan silhouettes, weighted leaf choices, and ore cutoffs
2026-09-06 03:56:26 -04:00
Brian Neumann-Fopiano 2967efbf35 Merged core stability fixes
- Object verification includes stable deposits and featurette fixes
2026-09-06 03:54:14 -04:00
Brian Neumann-Fopiano b3c3a0b183 Bounded feature preparation memory
- Stage at most 32 deposit clumps before ordered placement
- Preserve configured attempt counts and parallel preparation
- Apply consistent sea surface output bounds
2026-09-06 03:48:32 -04:00
Brian Neumann-Fopiano 420148fcc7 Bounded deposit preparation
- Deposit preparation retains at most 32 clumps between ordered placement passes
- Large deposit counts retain parallel preparation and stable per-clump seeds
- Overlap tests cover every attempt across preparation windows
- Deposit tests isolate platform block conversion from cached registry fixtures
2026-09-06 03:46:25 -04:00
Brian Neumann-Fopiano 4c989fe1c0 Merged object stability fixes
- Keep scaling geometry and volume in one locked snapshot
- Skip unsupported block orientations and their tile data
- Replace smart bore executor scaffolding with ordered loops
2026-09-06 03:44:26 -04:00
Brian Neumann-Fopiano 1a2c8a8845 Simplified object smart boring
- Smart boring runs direct axis passes under the existing write lock

- Negative object bounds stop at the last occupied coordinate

- Bore insertion avoids redundant map callbacks

- Regression tests cover ordered fills and repeated calls
2026-09-06 03:43:25 -04:00
Brian Neumann-Fopiano 606aa2e704 Fixed object transform stability
- Placement skips blocks omitted by unsupported rotations

- Rotated object copies omit matching tile data

- Scaling captures geometry and volume under one read lock

- Regression tests cover omitted orientations and concurrent scaling
2026-09-06 03:43:08 -04:00
Brian Neumann-Fopiano 5baea8dd5b Bounded sea surface decoration
- Sea surface decorators respect engine and output ceilings in every placement mode
- Clipped sea stacks retain their top palette
- Tests cover short outputs negative bases and engine ceilings
- Multi-palette tests verify shared random sources match fresh per-block sources
2026-09-06 03:40:27 -04:00
Brian Neumann-Fopiano 75353d43c7 Bounded sea surface decoration
- Sea surface decorators respect engine and output ceilings in every placement mode
- Clipped sea stacks retain their top palette
- Tests cover short outputs negative bases and engine ceilings
- Multi-palette tests verify shared random sources match fresh per-block sources
2026-09-06 03:36:35 -04:00
Brian Neumann-Fopiano 3f9759b05f Merged procedural feature improvements
- Simplify tree fungus and coral generation
- Enforce sea decorator limits and deterministic deposit writes
- Restore branch tip accents outside terminal leaves
2026-09-06 03:34:28 -04:00
Brian Neumann-Fopiano b9b969f1f9 Simplified procedural feature generation
- Fungi build stem roles directly without intermediate maps or coordinate arrays
- Tree palettes reuse one random source per object
- Persistent leaves skip distance traversal when plausibility is disabled
- Tree support scans avoid copied coordinate sets
- Coral arm samples reuse their endpoint array
- Sea decorators share scaled stack limits and respect output bounds
- Unresolved sea floor palette entries preserve existing water
- Tests cover stem geometry persistent leaves and sea stack limits
2026-09-06 03:31:54 -04:00
Brian Neumann-Fopiano 1e3831263c Restored branch tip decorators
- Branch tip accents occupy the first free outward cell beyond their leaf cluster
- Existing wood leaves and accents retain their positions
- Tests cover opposite branch directions and repeated placement
2026-09-06 03:30:29 -04:00
Brian Neumann-Fopiano ebaa616536 Restored branch tip decorators
- Branch tip accents occupy the first free outward cell beyond their leaf cluster
- Existing wood leaves and accents retain their positions
- Tests cover opposite branch directions and repeated placement
2026-09-06 03:29:45 -04:00
Brian Neumann-Fopiano 79690edaaf Stabilized deposit overlap order
- Clumps prepare in parallel and place in dimension region biome order
- Placement evaluates host rules against preceding deposits
- Each clump uses independent deterministic random state
- Configured attempt counts are sampled once per chunk
- Block traversal reuses cursor coordinates
- Tests cover reversed worker completion and generation context cleanup
2026-09-06 03:28:02 -04:00
Brian Neumann-Fopiano 6276bb7327 Merged compact biome selection
- Replace expanded rarity lists with cumulative counts
- Preserve ordinary selection mapping and bound extreme rarity storage
2026-09-06 03:27:31 -04:00
Brian Neumann-Fopiano 587e0fdee0 Merged hydrology planning improvements
- Reuse downstream route distances and primitive footprint indexes
- Bound diagnostic history and reduce containment bookkeeping
2026-09-06 03:27:31 -04:00
Brian Neumann-Fopiano 47f77df0e8 Stabilized deposit overlap order
- Clumps prepare in parallel and place in dimension region biome order
- Placement evaluates host rules against preceding deposits
- Each clump uses independent deterministic random state
- Configured attempt counts are sampled once per chunk
- Block traversal reuses cursor coordinates
- Tests cover reversed worker completion and generation context cleanup
2026-09-06 03:27:23 -04:00
Brian Neumann-Fopiano 534c844a7d Compacted biome selection
- Biome and carving children share compact rarity selection
- Weighted choices preserve alternating insertion order
- Rarity totals use long counts without expanded lists
- Natural child selection preserves its existing noise calls
- Differential tests cover normal and extreme rarity values
2026-09-06 03:24:46 -04:00
Brian Neumann-Fopiano b7e7f64bcf Bounded hydrology diagnostics
- Cold-query log suppression retains a bounded window of tile keys
- Hydrology closure releases cold-query diagnostic history
2026-09-06 03:22:07 -04:00
Brian Neumann-Fopiano 20b9b3dce3 Reduced hydrology planning work
- Downstream route lengths are evaluated once per routing grid
- Footprint construction sorts primitive column keys without a temporary tree
- Footprint lookups use primitive keys and preserve immutable ordered views
- Closed cave components keep one ordered flood-fill membership set
- Surface pools and rivers share their layer construction
- Regression tests cover routing limits, cycles, coordinate bounds, and footprint ordering
2026-09-06 03:21:55 -04:00
Brian Neumann-Fopiano a68cad5893 Merged terrain assembly improvements
- Ceiling layer thickness resolves independently from surface layers
- Locked strata repeat correctly above the internal height reference
- Cave scratch releases generation references after each pass
- Terrain actuators omit redundant setup and checks
2026-09-06 03:16:51 -04:00
Brian Neumann-Fopiano 3ac441484a Merged hydrology shutdown fixes
- Hydrology stops admitting plans during shutdown
- Active planning completes before mantle data is released
2026-09-06 03:16:16 -04:00
Brian Neumann-Fopiano c27a233a50 Retained active generation resources
- Failed planner drains retain mantle and generation data for retry
- Detached runtime cleanup retains its target until runtime teardown succeeds
- Terrain caches clear after hydrology shutdown completes
2026-09-06 03:16:16 -04:00
Brian Neumann-Fopiano 23bad0e1a2 Refined terrain assembly
- Ceiling layers use their own thickness generators
- Pack validation accepts independent ceiling layer counts
- Locked strata repeat across the reference height
- Surface and cave palettes share layer emission
- Cave passes release retained chunk sections
- Nested carving preserves outer blend buffers
- Profile blending reuses bounded numeric buffers
- Terrain actuators remove unused state and duplicate checks
- Regression tests cover layers and carving scratch lifetimes
2026-09-06 03:15:57 -04:00
Brian Neumann-Fopiano dde3c149ad Fixed hydrology shutdown
- Hydrology close drains active tile plans and column composition before returning
- Queued tile plans cancel without waiting for their executor to start them
- Closed caches reject new tile and column demand
- Shutdown tests cover background plans, queued demand, active composition, and recursive close
2026-09-06 03:15:15 -04:00
Brian Neumann-Fopiano 80155cef93 Merged object placement improvements
- Native structure protection uses transformed object bounds
- Object copies share the volume mutation lock
- Upscaling writes expanded voxels without temporary lists
2026-09-06 03:10:04 -04:00
Brian Neumann-Fopiano 057d51f211 Completed offline decorator hooks
- Offline generation resolves decorator support without Bukkit classes
- Probe coverage checks solid, fluid and air spike support
2026-09-06 03:10:04 -04:00
Brian Neumann-Fopiano 23316d2d5e Reduced object scaling allocations
- Enlarged objects write expanded cells through one reusable coordinate
- Scaling removes temporary voxel lists and unused bound reads
- Expanded tiles retain independent copies and existing write order
2026-09-06 03:08:00 -04:00
Brian Neumann-Fopiano d55c77572b Fixed object placement bounds
- Native structure vetoes use the full transformed object bounds
- Object copies hold the volume read lock during mutation-sensitive reads
- Object copies retain independent tiles without redundant coordinate clones
- Regression tests cover oblique rotations and off-center object origins
- Concurrent copy tests verify volume lock ownership
2026-09-06 03:08:00 -04:00
Brian Neumann-Fopiano 942545dcb9 Fixed mantle failure barriers
- Worker barriers propagate failed shared component tasks
- Cancelled shared tasks stop generation before later passes
- Local worktree directories remain excluded from source control
2026-09-06 03:06:52 -04:00
Brian Neumann-Fopiano 17c927acdd t 2026-09-06 02:33:09 -04:00
Brian Neumann-Fopiano 13263f1a03 Removed Iris CI
- Removed the GitHub Actions workflow for pushes and pull requests
2026-09-05 21:48:50 -04:00
Brian Neumann-Fopiano 3a0e4557b7 Fixed cave biome identity
- Missing cave biome identities use the column surface biome
- Capture tests cover omitted caves, configured caves, and invalid surface keys
- Runtime scope tests retain keyed cave biome fixtures
2026-09-05 21:18:05 -04:00
Brian Neumann-Fopiano f8cbbde683 Merged Iris updates
- Historical biome retention is integrated

- Replacement staging progress and admission fixes are integrated
2026-09-05 20:37:46 -04:00
Brian Neumann-Fopiano 3f1448dc33 Merged replacement staging fixes
- Replacement progress and admission fixes are integrated with retained biome behavior
2026-09-05 20:31:17 -04:00
Brian Neumann-Fopiano 80843114fc Merged biome retention
- Saved biome behavior is integrated with current Iris changes
2026-09-05 20:30:52 -04:00
Brian Neumann-Fopiano b17fe811f3 Preserved historical biome behavior
- Generated chunks retain surface and vertical biome assignments
- Floating biome assignments persist through terrain transitions
- Archived pack definitions remain available for saved biome rules
- Spawns and effects use each saved biome environment
- Saved biome lookups load asynchronously with bounded caches
- Missing biome identities recover only from exact recorded evidence
- Effects and entity placement follow existing world surfaces
- New pack fingerprints ignore Finder metadata
- Tests cover biome storage, recovery, consumers, and scoped definitions
2026-09-05 20:30:50 -04:00
Brian Neumann-Fopiano 27d511d99a Fixed replacement staging stalls
- Concurrent replacement requests report busy before waiting for the manager lock
- Replacement staging reports localized phases and elapsed time
- Player progress messages use entity scheduling
- Pack copies flush completed files after traversal
- Replacement admission and progress tests cover scheduling and cleanup
- Language editor tests use a local catalog response
2026-09-05 20:30:30 -04:00
Brian Neumann-Fopiano c53e9f8261 Wand fix for WE 2026-09-05 20:24:01 -04:00
Brian Neumann-Fopiano 064ec0cae9 Merged noise refinement
- Existing noise corrections and fifteen new styles are integrated
2026-09-05 19:38:41 -04:00
Brian Neumann-Fopiano 14cfe3ed68 Added diverse noise styles
- Dune and Strata add dunes and layered bands
- Wood and Marble add warped grain and mineral veins
- Gabor and Scales add oriented waves and overlapping scallops
- Chladni and Kaleidoscope add nodal lines and mirrored motifs
- Menger Sponge and Circuit add recursive voids and connected traces
- New styles use the shared scale and full seeds
- Tests cover tile seams, volume variation, bounds, and octaves
2026-09-05 19:33:53 -04:00
Brian Neumann-Fopiano 645edcff5c Added patterned noise
- Gyroid and Quasicrystal provide labyrinths and wave interference
- Truchet tiles join quarter-circle ribbons
- Crater and Vortex provide bowls and spiral fields
- Pattern samplers share bounded octave accumulation
- Tests cover seeds, geometry, volume sampling, and world edges
2026-09-05 19:33:14 -04:00
Brian Neumann-Fopiano 50894b90ce Refined existing noise
- Hexagon styles produce coherent solid cells with contained subdivisions
- Sierpinski triangles use equilateral recursive geometry
- Noise styles share a 64-block base scale
- Zoom scales the complete pattern with its distortion
- Octaves preserve the base seed and feature scale
- Coordinate dispatch uses the native dimensional sampler
- Classic kernels retain seed precision and bounded output
- Interpolation preserves fractional coordinates and symmetric curves
- Noise caches respect sampling bounds and generation identity
- Clover sampling reuses vertices and cached trigonometry
- Noise and terrain tests cover corrected sampling behavior
2026-09-05 19:32:13 -04:00
Brian Neumann-Fopiano f2887632fe Dicing things up a little
- Trying to slim this beauty down to a size 6.
- Shes a size 7.1
2026-09-05 16:45:31 -04:00
Brian Neumann-Fopiano d10d70a894 Merged terrain generation updates
- Existing worlds retain generated terrain across generator updates
- New terrain blends at generation boundaries
- Cold generation and Studio startup improvements are integrated
- Terrain sampling validates active generator bounds and noise
2026-09-05 12:32:03 -04:00
Brian Neumann-Fopiano 5a5e8a8dc8 Validated terrain samples
- Terrain sampling rejects non-finite active bounds and noise before caching heights
- Validation tests cover invalid samples and unchanged finite arithmetic
- Bukkit packaging uses the existing 8 MB artifact limit
2026-09-05 12:31:36 -04:00
Brian Neumann-Fopiano 965606bbcc Merged generation improvements
- Cold startup and hydrology changes integrate with terrain history
- Flat authoring worlds skip native features under the history write guards
- Native decoration checks retain recorded structure and terrain ownership
- Automatic build revisions replace manual source seals
2026-09-05 12:05:14 -04:00
Brian Neumann-Fopiano b1e1fbc15e Preserved terrain across updates
- Saved chunks retain their natural geometry and generation ownership
- Future chunks blend against frozen terrain boundaries
- Build revisions capture generation source and dependencies automatically
- Native terrain queries use contained transition geometry and final height caches
- Historical searches retain recorded pack identities and placement facts
- World creation and Studio updates preserve native lifecycle ordering
- Hydrology containment and scoped content workers retain generation context
- Pregeneration validates saved completion before trusting cached progress
2026-09-05 11:57:01 -04:00
Brian Neumann-Fopiano 73d640943c Improved cold generation
- Flat authoring studios skip terrain and native structure preparation
- Normal Studio entry prepares the exact hydrology column asynchronously
- Hydrology prefetch admits the complete frontier before stale queued work
- Concurrent hydrology loads avoid cache monitor contention
- Cave density tasks drain failures before releasing chunk state
- Terrain and interpolation queries reuse exact bounded samples
- Pregeneration counts requested chunks directly from its bounds
2026-09-05 11:57:01 -04:00
Brian Neumann-Fopiano 20ad49b9d2 Merged generation history
- Integrated immutable generation epochs after dimension stacking
- Preserved historical terrain, biome, structure, and locate behavior
- Validated combined Bukkit and modded generation contracts
2026-09-04 15:07:18 -04:00
Brian Neumann-Fopiano 56b5f4dd0b Merged generation history
- Immutable generation epochs now coexist with dimension stacks
- Historical terrain, biomes, structures, and locate behavior remain stable across updates
- Versioned generation kernels preserve the combined runtime contract
2026-09-04 15:06:55 -04:00
Brian Neumann-Fopiano 8247f167cf Merged dimension stacking
- Upright stacked dimension generation is now part of master
- Superflat generation coverage is now part of the Iris test suite
2026-09-04 14:23:38 -04:00
Brian Neumann-Fopiano 6549b40797 Added dimension stacking
- Dimension files define upright top-to-bottom terrain layers
- Spacer values set exact air gaps between layers
- Optional noise styles shape blended layer boundaries
- Runtime queries resolve stacked terrain and biome ownership
- Pack validation rejects invalid stack definitions
- Project copy and hotload paths include stack dependencies
- Superflat fixtures cover gaps, blending, clipping, and fluids
- Platform tests cover stacked world generation contracts
2026-09-04 14:23:15 -04:00
Brian Neumann-Fopiano 30113c4133 Added generation history
- World-local pack epochs preserve generated chunks across restart-only updates
- Kernel catalogs and source seals retain executable generation behavior across jar changes
- Crash-safe ownership, boundary, registry, semantic, and mantle records survive activation changes
- New terrain blends lower surfaces and inverted ceilings against frozen historical boundaries
- Cave, object, structure, biome, and hydrology work respects activation ownership
- Find commands merge recorded facts with eligible active predictions for biomes, regions, structures, objects, and rivers
- Bukkit, Fabric, Forge, and NeoForge route generation through retained runtime scopes
- Shutdown ordering preserves class-loader resources until world and service cleanup finishes
- Tests cover activation chains, failure recovery, retained registries, bounded lookup, and transition geometry
2026-09-04 13:57:39 -04:00
Brian Neumann-Fopiano c1bcdd0a9d Reduced jar container overhead
- Seekable ZIP output removes redundant data descriptors without changing entry contents
- Compaction validates entry size and CRC before replacing the artifact
- Tests cover metadata preservation, deterministic output, corrupt payloads, and duplicate entries
2026-09-04 13:36:14 -04:00
Brian Neumann-Fopiano 366033a535 CORRECTIONS
- this is the corrections from the afoemrnioned commits
2026-09-04 00:02:43 -04:00
Brian Neumann-Fopiano a771fa6856 BuildFix 2026-09-03 16:38:23 -04:00
Brian Neumann-Fopiano a368d42558 Terrain is important
- Fixed Ores generating all over the surface.
- fixed UI bug
2026-09-03 15:20:51 -04:00
Brian Neumann-Fopiano 0510c108be Disregard. 2026-09-03 14:01:45 -04:00
Brian Neumann-Fopiano e4ca7d4e70 Merge feature/version-compat: gate pack content on the running Minecraft version
- Every registrant is evaluated on load (IrisData.preprocessObject ->
  IrisRegistrant.evaluateCompat) by a cached per-class reflective walker over the
  annotated key fields; the verdict is stored on the registrant and pool sites filter
  with isCompatExcluded(). Block keys resolve through the live registry, the legacy
  rename table, IrisDimension.blockFallbacks and the entry backup, in that order.
- Unit of exclusion is the compat unit that composes the missing content (registrant
  or object placement). A referencing unit drops the reference; an emptied required
  pool excludes the container (CompatPools cascade through regions, biomes, dimension,
  spawners, loot tables, jigsaw pools and structures). A cascade that reaches the
  dimension is a blocking validation error and world creation is refused.
- Object placements are evaluated per object from the .iob palette header: an object
  is excluded from a placement's pool unless a chance-1 type-replace (edit rule
  matching the key) or a fallback covers the key. edit find lists and marker mark
  lists are match-only. Missing palette keys stay MissingBlockState placeholders
  (air everywhere, real air nativeHandle) so a chance-1 edit rule can still rewrite
  them at placement time.
- Approved decisions: exclude the composing unit; console-only reporting; no version
  fields on packs and no cross-version pre-check (authors build on their version,
  older servers gate at load).
- IrisDimension.blockFallbacks (base key -> full block state) applies to every
  palette, decorator, deposit, replace rule and object in the pack.
- IrisBlockData.backup is revived on every platform (it was dead on Bukkit because
  B.getState returned air).
- The legacy rename table now lives in LegacyBlockRenames and is applied identically
  on Bukkit and the mod loaders.

Operator surface
- Boot listing per pack after the validation lines (IrisWorldGeneratorResolver on
  Bukkit, ModdedStartup on modded), grouped by key, capped at three subjects per key.
- Validation summary line on /iris pack validate and /iris pack status, findings
  persisted in PackValidationResult through PackValidationCache.
- One summary line per world engine when its pack has findings.
- /iris pack compat [pack] (alias cp) on Bukkit and modded, uncapped, from the
  persisted validation so no pack is reloaded; catalog keys in all 17 locales.

Verification on the branch
- Full build green in the .compatwt worktree; modded adapters compile.
- Golden hash unchanged on Paper 26.2, seed 1337, radius 8.
- Live boots on Paper 26.1.2 and 26.2: the stock Overworld lists 6 unavailable keys
  on 26.1.2 and none on 26.2; the world creates and generates without the excluded
  content on 26.1.2.

Integration on top of master (df34f328e..fc3a26dd9)
- Conflicts: PackValidator keeps both import sets (JSONObject for static object
  validation plus the compat and loader types); RiverPolicyResolver.State.apply takes
  the confinement scope and the pack data (confined rivers and gate-filtered river
  biome selections); IrisRiverPolicy keeps shoreBiomeWidth and confined next to
  compatBiomes.
- Static objects honour the gate. The per-object palette evaluation moved out of
  IrisObjectPlacement into the shared IrisObjectCompat helper; IrisStaticObject
  .evaluateCompat mirrors a one-object placement, records findings with subject type
  "static object" and the object key (DROPPED for uncovered palette keys, EXCLUDED
  for a missing edit replace palette), IrisStaticObjectLayer.compile skips gated
  entries instead of failing, and PackValidator.gateContent asks every dimension for
  its static objects (IrisDimension.evaluateStaticObjectsCompat) so the boot listing
  is complete. CompatWalker treats IrisStaticObject like IrisObjectPlacement: never
  descended, so a static object's edit palette excludes only that entry, never the
  dimension. Covered by IrisStaticObjectCompatTest.
- The modded compat and cp subcommands run inside ModdedCommandTree.localized like
  the rest of the pack tree and are documented in the modded help; the parity test
  asserts both.
- Locale keys stay in core/src/main/resources/languages: master's on-demand download
  reads those files from the repository and the build only keeps them out of the jar,
  so the compat keys sit next to the debugdump keys in the same form.
- Placeholder audit of master's new code: the only native casts added are the
  isFaceSturdy fallbacks in IrisSpeleothems and ModdedDecoratorHooks, both behind
  B.isAir guards, and MissingBlockState.nativeHandle is the real air anyway;
  IrisObjectTransforms only asks B.isAir; decorators resolve palettes through
  IrisBlockData.getBlockData, which never yields a placeholder. No new guards needed.
- core/purity-allowlist.txt gains IrisSpeleothems: 5053a644c split the Bukkit
  isFaceSturdy fallback out of the already allowlisted DecoratorCore without moving
  the allowlist entry, so master's own build failed the Bukkit purity ratchet.
- CompatTestPlatform states now answer isAir, isSolid, isFluid and materialKey like a
  real adapter: B caches the first registries it sees per JVM, so an interned mock
  air that never said isAir leaked into the core.compat tests once the static layer
  ran under that platform.

Gate commands on the merged tree
- ./gradlew build (default VolmLib composite, ../VolmLib at 7b8029e): BUILD SUCCESSFUL;
  core 3499 (8 skipped) tests, bukkit plugin 343 tests, nms v26_2_R1 255 (1 skipped) tests,
  probe 78 tests, buildSrc 10 tests, 0 failures.
- cd adapters/fabric && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/forge && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/neoforge && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/forge && ../../gradlew test --tests IrisModdedCommandParityTest --tests
  ModdedLanguageCommandsTest: BUILD SUCCESSFUL, 11 tests, 0 failures.
2026-09-03 10:48:37 -04:00
Brian Neumann-Fopiano fc3a26dd9e Ignore the compat lane worktree
The .compatwt/ directory holds a generated worktree for the
version-content compat lane and is never source.
2026-09-03 05:03:33 -04:00
Brian Neumann-Fopiano 657aaeaccf Add /iris language and /iris debugdump on both platforms
/iris language opens a locale menu and sets the language for the caller
or, for a gamemaster, the server default; /iris debugdump writes a
diagnostic report and optionally uploads it. Neither needs iris.all, so
a plain player can reach them. Every modded command now executes inside
a language audience scope, and the Bukkit dispatcher opens one around
execution, tab completion and help. Permissions, message keys and the
shipped locales follow.
2026-09-03 05:03:28 -04:00
Brian Neumann-Fopiano b25937f986 Download translations on demand instead of bundling them
Locales are fetched from the Iris repository at runtime into
languages/downloaded and cached against the revision the jar was built
from; the artifact ships only a manifest of the available locales.
IrisLanguage gains a player language service, per-audience snapshots and
an in-game message editor that writes locale overrides. The Bukkit
artifact verifier now fails when any locale file is embedded, and the
modded shadow jars drop the Bukkit-only switcher and editor classes.
2026-09-03 05:03:21 -04:00
Brian Neumann-Fopiano 5053a644c7 Grow speleothems from decorators and keep them supported
Dripstone and sulfur spikes are placed as oriented, thickness-graded
columns by the surface and ceiling decorators through a shared
IrisSpeleothems helper, replacing the ad hoc dripstone fixups, and the
stacking path is unified for both directions. The perfection modifier
renormalizes a spike whose support a pool replaced, dropping the
unsupported run and re-tipping what remains. Surface sturdiness is asked
per face now, so a downward spike tests the ceiling it hangs from.
2026-09-03 05:03:14 -04:00
Brian Neumann-Fopiano fd66b41bad Answer server-thread terrain queries from natural terrain
A height or biome query from the tick thread no longer blocks on a cold
hydrology plan: it answers from the natural streams and asks the
planning pool for the tiles meanwhile. A new world prefetches the tiles
around spawn when the generator is injected, and the hydrology pool gets
full parallelism instead of a third. Also stops the shared corner-bounds
cache from pinning a non-finite sample and fences its two stamp reads.
2026-09-03 05:03:07 -04:00
Brian Neumann-Fopiano a4fe527d65 Polish surface rivers: estuaries, ponds, erosion, sea caves
Surface rivers gain a drowned inlet at the mouth, a spring pond and an
inland terminal pond, a configurable erosion profile (thalweg fraction,
blend curve, bed noise, smoothing) and a per-area shore biome width;
banks lose freeboard and inset for a single sink. Underground courses
accept tributaries, coastal grottos can be admitted as standalone sea
caves, and a region or biome can confine the rivers born in it. Tile
planning forks its ring work through HydrologyForkJoin. Pack validation,
the schema, the probes and the tests follow.
2026-09-03 05:03:00 -04:00
Brian Neumann-Fopiano e92c0efe09 Keep native structures out of static object cells
Vanilla structure and feature placement now runs through the bounded
worldgen access with a protected-position predicate, so a jigsaw piece
or an imported feature can read a static object's blocks but never
overwrite them, and never schedules a tick or block entity there.
Dimensions without static objects keep the plain level as before.
2026-09-03 05:02:53 -04:00
Brian Neumann-Fopiano 0a791bb8bf Place static objects at absolute world coordinates
A dimension can now list staticObjects: an object key, a world position, a
fixed rotation and scale, block edits and an optional bore or smart bore.
The layer compiles once per dimension and stamps its blocks into every
newly generated chunk it covers, later entries winning overlaps. Pack
validation, the schema, the export closure and the object transforms
(origin-anchored scaling, tile rotation) cover the new fields.
2026-09-03 05:02:45 -04:00
Brian Neumann-Fopiano 0b543bf7ee Compat report, validation and commands
- Force-load every registrant and placement during pack validation and persist the findings with the Minecraft version in the validation cache
- Turn an excluded dimension into a blocking validation error and suppress content-key warnings the gate already reported
- Print the capped per-key listing after the validation lines on Bukkit and modded, ordered excluded, dropped, substituted
- Log one summary line per world engine from the published validation result
- Add /iris pack compat on Bukkit and modded with help and locale keys for every shipped language
2026-09-03 04:54:38 -04:00
Brian Neumann-Fopiano 0d464aabf3 Gate pools, placements and structures
- Filter excluded biomes, regions, spawners, loot tables, jigsaw pieces and structures out of every pool through CompatPools and cascade empty pools to the container
- Evaluate each object placement per object from the .iob palette header, honouring chance-1 edit rules and reporting every missing palette key
- Treat edit find lists and marker mark lists as match-only
- Exclude jigsaw pieces on missing palette keys, drop them from pools and exclude structures whose start pool is gone
- Drop custom biome spawns whose entity is missing before the datapack JSON is written
- Skip excluded placements in the object, floating object, tree and locator paths while keeping the per-chunk RNG draws unchanged
- Keep the engine focus biome and region null when the gate excluded them
2026-09-03 04:54:35 -04:00
Brian Neumann-Fopiano 516ef71c9c Version content gate core
- Add the ContentGate, CompatWalker, PackCompatReport and finding types under core.compat
- Resolve block keys through the live registry, the legacy rename table, dimension blockFallbacks and the entry backup on every platform
- Move the legacy rename table into LegacyBlockRenames so Bukkit and the mod loaders apply it identically
- Evaluate every registrant on load from IrisData.preprocessObject and store the verdict on IrisRegistrant
- Add IrisDimension.blockFallbacks as a plain object property
- Keep missing palette keys as MissingBlockState placeholders so a chance-1 edit rule can still rewrite them
- Guard the modded rotator, merger and face fixer against non-modded states
- Source the legacy potion effect aliases from the gate so the Bukkit-typed table stays unloaded on modded
2026-09-03 04:54:22 -04:00
Brian Neumann-Fopiano df34f328e7 Pregeneration throughput
A pregeneration now generates in multicore mode: the matter stage runs
on the calling thread so its mantle window fans out across the burst
pool while biome and terrain run alongside, components are claimed
through the in-flight map so overlapping windows join instead of
blocking, and each region's chunks are ordered in stride-4 lattices so
consecutive chunks never share a window. Hydrology tiles for the whole
area are planned ahead nearest the centre first on a dedicated planning
pool, and the ring around every needed tile stays planned as the front
moves. Height bounds at grid corners are shared between threads and
from planning into generation, the sole engine of a pack is answered
from a snapshot instead of the registry lock, composed chunk keys are
mixed, cached tiles are answered in place, the burst pool grows to two
workers per core for the run, and large chunk-worker pools admit up to
256 requests.
2026-09-02 13:56:58 -04:00
Brian Neumann-Fopiano 88bd8feeeb A hydrology tile that fails to plan generates without rivers
Planning failures used to escape into the chunk system and crash the
server. The tile cache now catches them, logs the failure in full and
publishes an empty tile so terrain keeps generating. A natural height
that is not finite is no longer memoized, and its error names the
region, biome, fluid height, overlay, every interpolator's bounds and
generator heights, and the cached and fresh values for the column.
2026-09-02 11:46:30 -04:00
Brian Neumann-Fopiano e63fd9c448 Describe the river placement, routing and grotto pool enums for the schema 2026-09-02 11:46:29 -04:00
Brian Neumann-Fopiano 1c577fc580 Fit the Bukkit artifact to VolmLib master
VolmLib master added remote language catalogs and TOML language tooling.
The TOML language editor and parser depend on the excluded config
package and nothing in Iris uses them, so they are excluded too, and the
artifact size budget rises to 7.5 MB for the shipped localization
classes.
2026-09-02 03:08:27 -04:00
Brian Neumann-Fopiano 665b201e41 Merge terrain-first hydrology into master
Surface rivers v2: valley-first eroded channels, spring pools, standing
pools, tributaries, bed padding, ring-ordered feature search, schema v2
hydrology configuration and the transect probe.
2026-09-02 03:00:10 -04:00
Brian Neumann-Fopiano 333b766d2d Datapack imports from Modrinth, HTTP(S) and local file ZIPs
Work in progress parked from the working tree so the hydrology merge
could land; amend or squash as needed.
2026-09-02 03:00:10 -04:00
Brian Neumann-Fopiano f6a7ad7472 Merge origin/master into terrain-first hydrology
Brings master's Sentry removal, bound platform classification, Vision
entity toggle, release version and generation context binding onto the
rivers rework while keeping the hydrology system master had removed.
Hydrology files master deleted are restored from the branch, master's
non-hydrology edits are kept, and the modded adapter builds drop the
Sentry dependency with master.

Also removes the ridge_tunnel find selector: no course publishes ridge
bores any more, so it could never return a result.
2026-09-02 02:52:08 -04:00
Brian Neumann-Fopiano d23d6d5c9b Tributaries join the main stem at a confluence
- routing.tributaries lets an outlet keep extra surface courses; each is cut where its centerline first meets the stem and joined with a short connector
- A tributary must keep half the minimum course length and arrive at or above the stem's water
- Surface shape rejections carry the failing rule as their detail
2026-09-02 00:54:22 -04:00
Brian Neumann-Fopiano 185e2fa4ae Planner probe test excludes standing pools from required coverage 2026-09-02 00:40:13 -04:00
Brian Neumann-Fopiano 3ec5fab70b Planner probe does not expect standing pools without pool settings 2026-09-02 00:32:25 -04:00
Brian Neumann-Fopiano 479b94d6a2 Standing surface pools
- hydrology.surfacePools defines bowls filled with their own fluid; riverPolicy.surfacePools opts areas in
- Pools are independent SURFACE_POOL courses with one STANDING_POOL segment, shaped by the erosion field, kept clear of rivers and off slopes
- Locators accept the pool selector and pool ids; validator, renderer, probes and docs follow
2026-09-02 00:29:32 -04:00
Brian Neumann-Fopiano 13bce7aa42 Rejection details on surface course diagnostics
Valley solver rejections carry the failing measure (required cut, exposed stations, missing head) through the planner diagnostics; the transect probe prints them.
2026-09-02 00:05:10 -04:00
Brian Neumann-Fopiano 8848167c22 Surface drainage never climbs beyond the permitted cut
- Lattice edges that rise more than the cut the valley solver accepts are not surface drainage
- Ocean reachability uses the same rule, so ground behind a ridge gets inland grotto outlets instead of an impossible mouth
- Transect probe renders a tile diagnostics map with the drainage graph and rejected candidates
2026-09-01 23:46:11 -04:00
Brian Neumann-Fopiano f4a76dfa85 Cost cross-slope drop in route refinement
Refined routes now pay for ground that falls away beside them, keeping channels on valley floors instead of hillside traverses the incision cap rejects.
2026-09-01 23:38:32 -04:00
Brian Neumann-Fopiano 4b497a7d20 Prefer longer routes when ranking river sources
routing.lengthPreference adds route length to the source score so outlets take their longest legal course.
2026-09-01 23:33:15 -04:00
Brian Neumann-Fopiano 6945cbfd9b Spring pool tests and single source marker
- Only the centerline cell of the first station carries the source feature
- Headwater and styled geometry tests expect the spring pool
- Transect table starts with the first 24 stations
2026-09-01 23:21:44 -04:00
Brian Neumann-Fopiano d1c73025a0 Spring pools at headwaters and ring-ordered feature search
- channel.springWidthRatio / springLength open every surface river in a pool that shrinks where the ground falls away
- Banks settings keep structural invariants only; authoring bounds stay in the validator
- Feature searches walk tiles outward and stop early; find and goto report progress per ring
2026-09-01 23:13:47 -04:00
Brian Neumann-Fopiano 896e58e212 Delete the dead v1 hydrology settings surface
- Surface inset, terrain blend and ridge tunnel settings gone; Hydraulics keeps only waterfallMinimumDrop
- Branching folded into Routing; refinementSpacing derived from the sample lattice
- Contained surface routing branch, surface incision helpers and 13 unreachable compiler members deleted
2026-09-01 23:08:47 -04:00
Brian Neumann-Fopiano 639786537b River bed padding and feature search rings
- surface.bed: falling blocks under and beside river water are replaced by a padding palette unless allowed
- Ring order helper for bounded feature searches
2026-09-01 23:08:47 -04:00
Brian Neumann-Fopiano 1ad3a25cb6 Object scatter test follows the channel-or-shore veto 2026-09-01 21:57:51 -04:00
Brian Neumann-Fopiano 507e241b2c Legacy hydrology tests on the v2 contract; water connects across steps
- Geometry pins became relational properties; tests for deleted mechanisms removed
- Every owned channel water column is published to the mantle so steps get fluid updates
- Upper beds at head steps reach the lower head so the water stays one body
- Ocean aprons are measured from the first ocean station and bounded laterally
- Dry cave headroom uses the flooded cave biome
2026-09-01 21:54:34 -04:00
Brian Neumann-Fopiano 3d96a8616a Hydrology probes on the rivers v2 contract
- Ocean integrity fires on land columns near a mouth; shallow incision and bank continuity are gates
- Probe-local centerline and turn helpers replaced by production geometry
- v1 checks for ridge bores, endpoint ramps, terminal caps, transition recovery and pool lengths removed
2026-09-01 21:39:46 -04:00
Brian Neumann-Fopiano 0c317929f8 Transect probe lists rejection reasons per tile 2026-09-01 21:32:48 -04:00
Brian Neumann-Fopiano 4c4153debd Valley shaping fixes from the first transects
- Head ring now covers every cell that can sit beside water
- Blend widths are smoothed along the course; the bed is a broad bowl
- Diagonal shore cells keep the lip; cross-section rows belong to their own station
- Transect probe prints coordinates and numeric section tables
2026-09-01 21:29:08 -04:00
Brian Neumann-Fopiano 124bcd9372 Align schema bounds with the hydrology validator
Studio schema annotations now match the validator ranges; mouthLevelingDistance must fit inside maximumRouteLength.
2026-09-01 21:15:36 -04:00
Brian Neumann-Fopiano 00f5a16c20 Validator for rivers schema v2 and dead planner code removal
- PackHydrologyValidator rewritten for surface.channel/banks/flow/mouths, routing weights and bankBiomes
- 76 unreferenced private members deleted from HydrologyPlanner and HydrologyFootprintCompiler
2026-09-01 19:30:29 -04:00
Brian Neumann-Fopiano b57a2e2fc8 Remove the 2D cache bypass
Hydrology sampling now goes through the engine 2D caches; the thread-local bypass and its test are gone.
2026-09-01 19:22:42 -04:00
Brian Neumann-Fopiano 5b1b636539 Point basis samples carry terrain slope
Routing, validation and the valley solver sampled basis columns with slope 0; the forward slope now rides on every point sample.
2026-09-01 19:21:25 -04:00
Brian Neumann-Fopiano 2585b2105a River transect probe
- riverTransectProbe renders each surface course as a plan and five cross-sections
- Summary gates ocean writes and spilling channel cells
- Grading columns now report the bank biome
2026-09-01 19:18:59 -04:00
Brian Neumann-Fopiano 676372efab River world hooks
- Skip nib, pothole, wall and slab post passes on river footprints and their collar
- Keep automatic object scatter off the channel and shore band
- Expose deeper biome layers on eroded banks and use the bank biome on the blend band
- Give dry cave headroom the flooded cave biome
2026-09-01 19:08:20 -04:00
Brian Neumann-Fopiano 8a256e3226 Surface course integration
- Build surface courses through the valley solver and segment labeler
- Compile surface footprints through the erosion field and index them for cave containment
- Drop the contained-surface fallback and the post-hoc surface rejection passes
- Choose the river profile at the source
2026-09-01 19:05:43 -04:00
Brian Neumann-Fopiano 2c6792977f Surface valley model
- Add the surface package with centerline densification, channel profile, valley head solver and erosion field
- Derive the head from the bank ring outside the channel so every wet cell is contained by construction
- Stamp one continuous cross-section with a cut-scaled blend and a freeboard lip, filtered at the ocean
2026-09-01 18:56:14 -04:00
Brian Neumann-Fopiano 915cc1a2f0 Surface settings v2
- Replace hydraulics, ridge tunnel, branching and inset ranges with channel, banks, flow and mouth configs
- Add routing cost weights, underground mouth leveling, bank biomes and bank multiplier
- Map the new schema into planner settings and derive refinement spacing
2026-09-01 18:46:12 -04:00
Brian Neumann-Fopiano 48afa0d93f Terrain-first hydrology
- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime
- Add the hydrology pack schema, validator, probes and tests
- Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
2026-09-01 18:28:00 -04:00
Brian Neumann-Fopiano 15bc52e9b5 V+ 2026-08-28 17:06:58 -04:00
Brian Neumann-Fopiano 9103796845 Toggle Vision entities
- Hide living-entity markers by default while retaining player markers.
- Add an Entities control beside Grid and stop polling hidden entity snapshots.
- Localize the new control and status messages across all bundled languages.
- God i gate these kinds of commit messages
2026-08-28 16:41:06 -04:00
Brian Neumann-Fopiano 27799039d1 Bind generation context
- Resolve first-generation engine binding from authoritative published level context.
- Validate generator identity and recover canonical Overworld binding from stale snapshots.
- Add shared loader regression tests and their Mockito test dependency.
2026-08-28 16:30:11 -04:00
Brian Neumann-Fopiano fd8c55076e Bridge host levels
- Publish and remove dynamic levels through host lifecycle methods when available.
- Attach hybrid host level data before engine binding and publication.
- Preserve native loader behavior when host extensions are absent.
2026-08-28 16:30:05 -04:00
Brian Neumann-Fopiano 95d8523d1d Route hybrid schedulers
- Select Bukkit scheduling only when the bound platform identifies as Bukkit.
- Route hybrid Youer hosts through the modded scheduler despite Bukkit classes on the classpath.
- Add regression coverage for Bukkit and NeoForge platform identities.
2026-08-28 16:29:58 -04:00
Brian Neumann-Fopiano ecec18509c Honor bound platform
- Route shared runtime hooks through the active Iris platform instead of classpath presence.
- Keep Bukkit-specific mantle and Matter behavior behind explicit platform capabilities.
- Cover hybrid classpaths and bound hook precedence with regression tests.
2026-08-28 16:29:53 -04:00
Brian Neumann-Fopiano 1ca943ab51 Remove Sentry
- Remove automatic error reporting and server-identifying attachments.
- Remove Sentry dependencies, release tooling, settings, and test commands.
- Keep failures operator-visible through local stack traces and logging.
2026-08-28 16:29:33 -04:00
Brian Neumann-Fopiano 1a561da2cd update gradl 2026-08-27 19:24:33 -04:00
Brian Neumann-Fopiano 2b92fbf825 iris release commits 2026-08-27 02:47:05 -04:00
Brian Neumann-Fopiano f398c55091 Remove Hydrology 2026-08-27 00:45:15 -04:00
Brian Neumann-Fopiano bdaa84975a f 2026-08-25 15:49:54 -04:00
Brian Neumann-Fopiano 07cc76614f Way too much 2026-08-25 12:52:12 -04:00
Brian Neumann-Fopiano 2797a5bf22 dwa 2026-08-24 16:50:57 -04:00
Brian Neumann-Fopiano 37b01b73c4 Imagemap sanitization 2026-08-24 13:44:44 -04:00
Brian Neumann-Fopiano e20bb86f40 f 2026-08-24 02:27:26 -04:00
Brian Neumann-Fopiano fe5651f854 d
d
2026-08-23 13:56:23 -04:00
Brian Neumann-Fopiano dce7af1955 object cleanup 2026-08-22 11:29:37 -04:00
Brian Neumann-Fopiano 770240e4f7 Merge pull request #1249 from VolmitSoftware/unification
Iris 4.0 unification: pure-JVM core + SPI with Bukkit/Fabric/Forge/NeoForge
adapters, art.arcane package layout, and the Gradle Groovy build.

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

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

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

Verification harness:
- /iris dev goldenhash: buffered chunk generation (no world writes), SHA-256
  per-chunk block+biome digests, golden capture/verify with per-chunk
  mismatch reporting, auto-diagnosis (repeat-gen + mantle-reset comparison,
  block-level diffs), deep per-chunk blockstate dumps, threads/deep params,
  full mantle reset (saveAll + tectonic file wipe) for reproducible runs
- IrisEngineSVC: mantle trim/unload suspended while a goldenhash scan runs
- MantleCarvingComponentTop2BlendTest: sequence-tiebreak regression test
2026-06-11 12:40:21 -04:00
Brian Neumann-Fopiano c5a2a57822 SMID 2026-06-10 23:42:12 -04:00
Brian Neumann-Fopiano 58d24ca55e dwa 2026-06-10 21:21:18 -04:00
Brian Neumann-Fopiano c6c02a9040 f 2026-06-10 13:52:09 -04:00
Brian Neumann-Fopiano a530a03459 nms patches 2026-06-10 09:00:23 -04:00
Brian Neumann-Fopiano a82b8f4a9c dwa 2026-06-09 20:14:27 -04:00
Brian Neumann-Fopiano cf2e7df298 c 2026-06-08 00:20:23 -04:00
Brian Neumann-Fopiano d20b3af9b9 addons 2026-06-07 15:23:41 -04:00
Brian Neumann-Fopiano 01d98d2141 Procedurality 2026-06-07 01:55:17 -04:00
Brian Neumann-Fopiano febe930db0 dwa 2026-06-07 00:04:20 -04:00
Brian Neumann-Fopiano 5c832ba128 ignore 2026-06-06 18:39:57 -04:00
Brian Neumann-Fopiano 03f9e233da No more. memory leak in pregen 2026-06-03 19:08:32 -04:00
Brian Neumann-Fopiano 11efae428f x 2026-06-03 01:21:14 -04:00
Brian Neumann-Fopiano 1a6fff35e8 fff 2026-05-31 19:40:17 -04:00
Brian Neumann-Fopiano 540a416bc9 Good points 2026-05-31 07:51:24 -04:00
Brian Neumann-Fopiano 7eb6e87a81 Perf Pass 1 2026-05-30 22:39:51 -04:00
Brian Neumann-Fopiano 9ff1c502ef dwa 2026-05-30 01:27:53 -04:00
Brian Neumann-Fopiano 08c6f7ddf9 lmao 2026-05-29 23:52:58 -04:00
Brian Neumann-Fopiano 4bc1913f03 redesign for Structures 2026-05-29 19:54:10 -04:00
Brian Neumann-Fopiano 821cc027db fix 2026-04-23 22:44:52 -04:00
Brian Neumann-Fopiano 1968ef2f2c Wip floaters 2026-04-23 21:29:23 -04:00
Brian Neumann-Fopiano ce29b70618 Content 2026-04-23 16:38:32 -04:00
Brian Neumann-Fopiano 3d128b70a7 RedoDeco 2026-04-22 19:23:50 -04:00
Brian Neumann-Fopiano 23fad24fb7 Whoops 2026-04-22 16:33:25 -04:00
Brian Neumann-Fopiano 6df718e6ca floatt 2026-04-22 09:42:45 -04:00
Brian Neumann-Fopiano 787c728060 Amending / removing all structures will redesign later im over this 2026-04-20 18:20:53 -04:00
Brian Neumann-Fopiano 88bbce82fe Object Studio and Plausibilizer 1.0 2026-04-18 14:50:36 -04:00
Brian Neumann-Fopiano e6a8351e57 Bulk changes and fixes
namely the chunk issue, and objects not wantingto place on cave tops.
2026-04-17 22:28:35 -04:00
Brian Neumann-Fopiano b82472d521 Loads 2026-04-17 16:14:49 -04:00
Brian Neumann-Fopiano 8c05f1bf1d polish 2026-04-15 16:35:49 -04:00
Brian Neumann-Fopiano 6a5616abae d 2026-04-15 15:52:18 -04:00
Brian Neumann-Fopiano 6096c39192 OptiPass3 2026-04-15 15:13:35 -04:00
Brian Neumann-Fopiano b42d0e4710 OptiPass 2
Smart Sampling and stuff
2026-04-15 13:26:52 -04:00
Brian Neumann-Fopiano dcb3306197 Opti pass 1 2026-04-15 12:50:12 -04:00
Brian Neumann-Fopiano 9a231b2bcf Studio FIxes 2026-04-15 09:45:02 -04:00
Brian Neumann-Fopiano aa706d027b WIP Building for latest.
Still Quantizing, WOrking on that next
also removed prebake
2026-04-15 09:05:41 -04:00
Brian Neumann-Fopiano 568fb07f66 gr 2026-04-13 17:06:29 -04:00
Brian Neumann-Fopiano bf207b7062 speed pass 2026-02-24 03:57:07 -05:00
Brian Neumann-Fopiano 18d4dce1db dwa 2026-02-22 23:25:01 -05:00
Brian Neumann-Fopiano 651dfa247e perfpass 2026-02-22 09:58:33 -05:00
Brian Neumann-Fopiano 5b1ff6d2f7 ugh 2026-02-22 09:08:31 -05:00
Brian Neumann-Fopiano 589baafc28 kts gone 2026-02-22 08:59:50 -05:00
Brian Neumann-Fopiano 130073989d Fix Shafts 2026-02-22 07:09:59 -05:00
Brian Neumann-Fopiano 2ca8cc7ad3 f 2026-02-21 18:56:10 -05:00
Brian Neumann-Fopiano 9f35855599 dwa 2026-02-21 04:14:18 -05:00
Brian Neumann-Fopiano 5110a33857 forgor 2026-02-21 03:50:43 -05:00
Brian Neumann-Fopiano a38e87cce3 f 2026-02-21 03:49:10 -05:00
Brian Neumann-Fopiano 1d09cd5f0f dwa 2026-02-21 03:37:44 -05:00
Brian Neumann-Fopiano 72c891ce5b dwa 2026-02-20 23:17:17 -05:00
Brian Neumann-Fopiano 3964185a81 Working 2026-02-20 17:53:11 -05:00
Brian Neumann-Fopiano afc5f67bc2 f 2026-02-19 22:35:29 -05:00
Brian Neumann-Fopiano 1c72d6410f f 2026-02-19 19:50:29 -05:00
Brian Neumann-Fopiano c2c612eb35 d 2026-02-19 16:49:54 -05:00
Brian Neumann-Fopiano 415c2f5837 JIGGY! 2026-02-19 04:09:06 -05:00
Brian Neumann-Fopiano b491d9efd0 im not getting Jiggy with it 2026-02-19 02:02:31 -05:00
Brian Neumann-Fopiano d643461e2e f 2026-02-19 01:23:08 -05:00
Brian Neumann-Fopiano 1f41e195cf p1 2026-02-18 23:44:23 -05:00
Brian Neumann-Fopiano 7b9280083f f 2026-02-18 23:43:02 -05:00
Brian Neumann-Fopiano 73525c170d Noise Pass 2026-02-18 23:10:49 -05:00
Brian Neumann-Fopiano 16c861a781 f 2026-02-18 20:56:30 -05:00
Brian Neumann-Fopiano d6bd5ec82f d 2026-02-18 20:24:25 -05:00
Brian Neumann-Fopiano 1829fa3eb1 d
d
2026-02-18 15:18:21 -05:00
Brian Neumann-Fopiano 8d215e3911 V 2026-02-18 15:08:55 -05:00
Brian Neumann-Fopiano ba819e4899 canvas checks 2026-02-17 22:44:01 -05:00
Brian Neumann-Fopiano 05d79b6d40 Create std 2026-02-17 06:18:55 -05:00
Brian Neumann-Fopiano cf64fbb730 Waow 2026-02-17 02:54:34 -05:00
Brian Neumann-Fopiano 43a65135b1 WIP - Dont compile 2026-02-17 00:34:34 -05:00
Brian Neumann-Fopiano 440c1bec6c "Folia" 2026-02-16 06:07:25 -05:00
Brian Neumann-Fopiano 0fd8bcc488 f 2026-02-15 17:43:28 -05:00
Brian Neumann-Fopiano bb1c0b8f68 FIXES 2026-02-15 04:49:23 -05:00
Brian Neumann-Fopiano fc25acdea2 CMD 2026-02-14 23:55:01 -05:00
Brian Neumann-Fopiano 0aa6f6d523 d 2026-02-14 19:58:45 -05:00
Brian Neumann-Fopiano a79c1a7954 ignore, just tracking info 2026-02-14 18:11:43 -05:00
Brian Neumann-Fopiano 7885762cd7 shakey shakey 2026-02-14 05:44:10 -05:00
Brian Neumann-Fopiano 86ed3f0095 OOPS
kinda need this
2026-02-14 04:52:30 -05:00
Brian Neumann-Fopiano 99fcc8fd03 WIP 2026-02-14 01:56:05 -05:00
4389 changed files with 676742 additions and 147844 deletions
+58 -47
View File
@@ -1,67 +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: mcversion
id: platform
attributes:
label: Minecraft Version
description: What version of Minecraft is the server on?
label: Server software
options:
- 1.14.X
- 1.15.X
- 1.16.X
- 1.17
- 1.17.1
- 1.18
- 1.19
- 1.20
- 1.21
- 1.22
- "Paper"
- "Purpur"
- "Leaf"
- "Canvas"
- "Folia"
- "Spigot"
- "Fabric"
- "Forge"
- "NeoForge"
- Other (unsupported)
validations:
required: true
- type: input
id: irisversion
- type: dropdown
id: minecraft
attributes:
label: Iris Version
description: What version of Iris are you running? (see console)
placeholder: DO NOT SAY "LATEST"
label: Minecraft version
options:
- "26.3"
- "26.2"
- "26.1.2"
- Other (unsupported)
validations:
required: true
- type: input
id: logs
- type: textarea
id: extra
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.
+71 -1
View File
@@ -10,4 +10,74 @@ libs/
collection/
/core/src/main/java/com/volmit/iris/util/uniques/
out/
tools/simd-bench/out/
tools/simd-bench/simd-bench.jar
DataPackExamples/
TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is
# server output; neither is ever source. No source directory in this repo is named run or logs.
run/
logs/
*.log
*.log.gz
crash-reports/
hs_err_pid*.log
replay_pid*.log
.codegraph/
plans/
.env
.env.*
!.env.example
local.properties
*.jks
*.p12
*.pfx
*.pem
*.key
*.hprof
credentials.json
service-account*.json
# docs/ is hand-written and tracked (flat numbered product docs). Nothing under it is
# generated, so there is nothing to ignore here.
.qa/
.repro/
.perf/
.release-gate/
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
.apiwt/
.papiwt/
__pycache__/
# Doc authoring scratch
.docs-wip/
AGENTS.md
# Local build tooling, not part of the project
build-lt.sh
BUILDS/
.run/
# Version-content compat lane worktree. Generated, never source.
.compatwt/
.worktrees/
/VolmLib/
/docs/
/.perf/
/.codegraph/
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

+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>.
+312 -54
View File
@@ -1,75 +1,333 @@
# Iris
The master branch is for the latest version of minecraft.
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
or NeoForge server mod. Cross-platform generation targets deterministic parity for identical
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.3.
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions)
# Building
Authoritative docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository, which is the source for the hosted wiki.
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.
Consider supporting development by buying Iris on Spigot.
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
## Language and localization
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
### Command Line Builds
## Platforms
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 21
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
3. Once the project has setup, run `gradlew iris`
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.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 |
### IDE Builds (for development)
Java 25 is required on every platform.
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
prefer.
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
workspaces with schema autocomplete over the server's live registries, entity spawning parity
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
the goldenhash determinism gate, which is interchangeable across all four platforms.
# Iris Toolbelt
### Native worldgen over Iris terrain
Everyone needs a tool-belt.
Iris replaces the chunk generator, so vanilla and mod worldgen only runs where Iris runs it. This
is identical on every platform.
```java
package com.volmit.iris.core.tools;
| Vanilla / mod worldgen | Over Iris terrain | Control |
|---|---|---|
| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies individual keys |
| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters |
| Carvers (caves, canyons, mod carvers) | Never - architectural | Iris has no `NoiseGeneratorSettings` for a carver to sample; use pack `caves`/`carvings` |
| Surface builders and surface rules | Never | Iris builds its surface from pack palettes |
| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter` or `biomeSkyScatter` target | Iris chooses biomes from the pack, not from a biome source |
| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's |
// Get IrisDataManager from a world
IrisToolbelt.access(anyWorld).getCompound().getData();
With `importedFeatures` off - the default - chunk output is byte-for-byte what Iris has always
produced. See [94 - API - Modded](https://github.com/VolmitSoftware/docs/blob/master/iris/94-api-modded.md) and
[01 - Installation & Platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) for the full control reference, including
which `pointed_dripstone` keys the 26.2 `speleothem` rename does and does not affect.
// Get Default Engine from world
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
Independently of that flag, Iris custom biomes now inherit the biome tags of their vanilla
derivative on every platform, so the emitted datapack tag files change. Anything driven by biome
tags therefore applies to Iris custom biomes: mob variants, spawn rules, and any vanilla or mod
content selecting on `#minecraft:is_overworld` and friends.
// Get the engine at the given height
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
## Install
// IS THIS THING ON?
boolean yes=IrisToolbelt.isIrisWorld(world);
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
the server. First boot performs no pack download. Run `/iris download pack=overworld`,
`/iris download pack=underworld`, or `/iris download link=https://host/path/pack.zip`, waiting for
each download to finish before starting another. The current built-in packs declare no external
datapack imports. Restart after download so Minecraft loads their dimension types and biomes.
Custom packs that declare `datapackImports` must complete their external-datapack installation and
registry restart first. Bukkit accepts Modrinth, direct HTTP(S), and absolute local `file:` ZIP URLs;
ZIPs under `plugins/Iris/datapacks/imports/` are discovered for every Iris dimension. Plain Spigot
supports ordinary managed `/iris create`, but not
the early-bootstrap `/iris replace` path for canonical Overworld, Nether, or End slots.
// GTFO for worlds (moves players to any other world, just not this one)
IrisToolbelt.evacuate(world);
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only
packs already on disk and never accesses the network. `/iris download` installs a pack atomically
without stopping the server. The current built-in packs declare no external datapack imports.
Custom packs that declare them require compatible archives in that save's `datapacks/` directory;
modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart only after
the Iris pack and all of its inputs are present. Packs register their custom dimension types,
height ranges, biomes, and external structure keys during that boot; worlds created
before it run with fallback registry data.
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
Create New World screen; the integrated server runs the same engine.
## The client mod
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
mod is inert.
## Quickstart
Complete the platform's pack, external-datapack, and registry-restart workflow above first. Then
create and enter an Iris world.
Plugin (optional arguments are keyed):
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
.name("myWorld") // The world name
.dimension("terrifyinghands")
.seed(69133742) // The world seed
.pregen(PregenTask // Define a pregen job to run
.builder()
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
.build())
.create();
```
/iris create myworld type=overworld seed=1337
/iris tp myworld
```
Mod (positional arguments):
```
/iris create myworld overworld 1337
/iris tp irisworldgen:myworld
```
On Paper-family servers with early bootstrap (not plain Spigot), the shipping pair can instead
replace the canonical portal-linked slots after their registry workflow is complete:
```
/iris replace minecraft:overworld type=overworld seed=123456789
/iris replace minecraft:the_nether type=underworld seed=-987654321
```
Restart once after both commands report staged. The independent seeds apply to their respective
slots, and vanilla portals keep routing between the canonical Overworld and Nether identities.
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
servers they are positional and composable:
```
/iris pregen start 352 world=myworld center=0,0 gui=false
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
```
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
console/status output. `/iris pregen status` reports progress on the plugin.
## Studio and VSCode workspace
The studio is the pack authoring environment, available on all platforms. Studio worlds are
transient - they are deleted on close and purged at startup.
Plugin:
```
/iris studio create name=<name> [template=<pack>]
/iris studio open <pack> [seed=1337]
/iris studio vscode [dimension=<pack>]
/iris studio update [dimension=<pack>]
/iris studio close
```
Mod:
```
/iris studio create [name] [template]
/iris studio open <pack> [seed]
/iris studio vscode [pack]
/iris studio update [pack]
/iris studio close
```
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
files hotloads the changes and regenerates the schemas.
## PlaceholderAPI
Iris registers the `iris` expansion when PlaceholderAPI is enabled. Paths are dot-separated,
lowercase, and never contain an underscore. Every value is plain text: no colour codes, no unit
suffixes, no `%` character, `.` as the decimal separator, and no thousands grouping.
Three answers are possible. A path that is not in the list below returns nothing, so PlaceholderAPI
re-emits the literal `%iris_...%` and a typo stays visible. A known path with no value right now
returns `---`. A real zero returns `0`.
| Placeholder | Value |
|---|---|
| `%iris_available%` | `true` when the Iris terrain service is live |
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
| `%iris_world.biome%` | Surface biome display name at the player, e.g. `Hot Desert Dunes` |
| `%iris_world.biome-key%` | Surface biome load key, e.g. `desert/hot-dunes` |
| `%iris_world.biome-custom%` | `true` when the surface biome defines custom derivatives, otherwise `false` |
| `%iris_world.biome-custom-id%` | Authored `customDerivitives[].id` when exactly one derivative is defined, for example `golden-dunes`; `---` for none or multiple |
| `%iris_world.biome-custom-ids%` | All authored custom derivative IDs, comma-and-space separated in definition order; `---` for none |
| `%iris_world.biome-custom-key%` | Namespaced Minecraft registry key for the sole custom derivative, such as `iris:biomes/<hash>`; `---` for none, multiple, or unavailable mapping |
| `%iris_world.biome-custom-keys%` | Registry keys for all custom derivatives in definition order, comma-and-space separated; `---` for none or any unavailable mapping |
| `%iris_world.biome-custom-count%` | Number of authored custom derivatives; `0` for a vanilla-only biome |
| `%iris_world.biome-derivative%` | Configured base derivative registry key, for example `minecraft:desert` |
| `%iris_world.biome-vanilla-derivative%` | Effective vanilla derivative registry key used when a vanilla biome is required |
| `%iris_world.biome-type%` | Inferred biome category: `land`, `sea`, `shore`, or `cave`; `---` when unavailable |
| `%iris_world.min-height%` | World minimum absolute Y, inclusive |
| `%iris_world.max-height%` | World maximum absolute Y, exclusive |
| `%iris_world.height%` | World vertical span, equal to maximum minus minimum height |
| `%iris_world.fluid-height%` | Dimension fluid level in absolute world Y; this is configuration, not a measurement of water at the player |
| `%iris_world.region%` | Region display name at the player |
| `%iris_world.region-key%` | Region load key |
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
| `%iris_pregen.available%` | `true` while a pregeneration job is running |
| `%iris_pregen.world%` | World name the running job is pregenerating |
| `%iris_pregen.percent%` | Completion, `0.00` to `100.00`, no `%` character |
| `%iris_pregen.eta%` | Estimated seconds remaining, whole number |
| `%iris_pregen.eta-text%` | Same estimate as `2m 5s` or `1h 30m` |
| `%iris_pregen.chunks%` | Chunks generated so far |
| `%iris_pregen.total%` | Chunks in the job |
| `%iris_pregen.chunks-per-second%` | Current rate |
| `%iris_pregen.paused%` | `true` while the job is paused |
Custom derivative IDs are authored metadata. A singular ID or registry key is available only when the biome defines one derivative; plural keys list all definitions. Saved columns retain their historical biome definitions.
Biome and region values describe the surface at the player's block column. Height values describe world configuration. Walking refreshes them at most
once per second per player, so a whole board of `world.*` keys costs one refresh per player per
second no matter how many of them are on it, and a value may lag a sprinting player by up to a
second. A jump that is not walking - joining, respawning, changing worlds, stepping through a portal,
or any teleport including `/iris goto`, `/tp`, an ender pearl and a random teleport - is published
immediately, so a player who arrives somewhere and then stands still never keeps reading the biome,
region or dimension of where they came from. `pregen.*` is global: there is one pregeneration job per
server, and `%iris_pregen.world%` says which world it is.
### Migration from the pre-2.0 keys
The old underscore keys are gone. There is no alias and no dual-accept window; an old key now
renders literally so it is visible rather than silently wrong.
| Old key | New key | Why |
|---|---|---|
| `%iris_biome_name%` | `%iris_world.biome%` | Renamed onto the dot grammar |
| `%iris_biome_id%` | `%iris_world.biome-key%` | Renamed; `id` was always the load key |
| `%iris_region_name%` | `%iris_world.region%` | Renamed onto the dot grammar |
| `%iris_region_id%` | `%iris_world.region-key%` | Renamed; `id` was always the load key |
| `%iris_biome_file%` | removed | Rendered an absolute server path into player-visible text, and threw on packs with no backing file |
| `%iris_region_file%` | removed | Same as `biome_file` |
| `%iris_world_seed%` | removed | Handed the world seed to anyone who could read a scoreboard, and a placeholder has no permission context to gate on |
| `%iris_terrain_height%` | removed | Reported the *generated* height, before objects and player edits, so it disagreed with the block under the player's feet |
| `%iris_terrain_slope%` | removed | Three extra noise samples per read for an unformatted pack-authoring diagnostic |
| `%iris_world_mode%` | removed | Studio or Production; a studio world exists for seconds during authoring and is never on a live board |
| `%iris_world_speed%` | removed | Mutated engine rate-window state every time it was read. `%iris_pregen.chunks-per-second%` answers the same question from a snapshot |
The old keys also read the *cave* biome for a player standing under an overhang, because they
sampled two blocks above the player's feet. The new `world.biome` is always the surface biome,
which is what a board reader means.
## Building from source
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```
builds every platform artifact into the workspace-level `../PluginOuts/` directory: the Bukkit jar
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
(1.21.1, 26.1.2, 26.2 and 26.3):
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
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 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
`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three
modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds
with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one
plugin classpath. Drive them with `-p`:
```
./gradlew -p adapters/fabric runServer # or runClient
./gradlew -p adapters/forge runServer
./gradlew -p adapters/neoforge runServer
./gradlew -p adapters/fabric test # shared adapters/modded-common test suite
```
Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system
properties:
| Flag | System property | Purpose |
|---|---|---|
| `-PirisParity=<pack>` | `iris.parity` | Run the cross-platform parity harness for a pack |
| `-PirisParityGolden=<file>` | `iris.parity.golden` | Compare against a captured golden-hash file |
| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only |
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
Shared code lives in `adapters/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
substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a
composite cycle. The build and release paths do not need it.
## Documentation
Full product docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository. Start with:
- [Overview and index](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md)
- [Installation and platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md)
- [Getting started](https://github.com/VolmitSoftware/docs/blob/master/iris/02-getting-started.md)
- [Structures overview](https://github.com/VolmitSoftware/docs/blob/master/iris/18-structures-overview.md)
- [API — Getting Started](https://github.com/VolmitSoftware/docs/blob/master/iris/90-api-getting-started.md)
- [Maintainer — MC version bump](https://github.com/VolmitSoftware/docs/blob/master/iris/85-maintainer-mc-version-bump.md)
- [Maintainer — release checklist](https://github.com/VolmitSoftware/docs/blob/master/iris/86-maintainer-release-checklist.md)
@@ -0,0 +1,137 @@
package art.arcane.iris.nativegen;
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;
import net.minecraft.world.level.levelgen.Heightmap;
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();
}
@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;
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;
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.NOISE);
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.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.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.FEATURES));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine.getComplex()::allowsMantleChunkWrite, chunk, ChunkStatus.SPAWN));
}
}
@@ -0,0 +1,485 @@
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;
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 applyCarvers(WorldGenRegion region, long seed, RandomState randomState,
BiomeManager biomeManager, StructureManager structureManager,
ChunkAccess chunk) {
}
@Override
public void buildSurface(WorldGenRegion level, StructureManager structureManager,
RandomState randomState, ChunkAccess protoChunk) {
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(
Blender blender, RandomState randomState,
StructureManager structureManager, ChunkAccess centerChunk) {
return CompletableFuture.completedFuture(centerChunk);
}
@Override
public int getSeaLevel() {
return 63;
}
@Override
public int getMinY() {
return -64;
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor heightAccessor, RandomState randomState) {
return 64;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
RandomState randomState) {
BlockState[] states = new BlockState[heightAccessor.getHeight()];
for (int index = 0; index < states.length; index++) {
states[index] = Blocks.STONE.defaultBlockState();
}
return new NoiseColumn(heightAccessor.getMinY(), states);
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState randomState,
BlockPos feetPos) {
}
}
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
StructureTemplate template = new StructureTemplate();
CompoundTag tag = new CompoundTag();
ListTag size = new ListTag();
size.add(IntTag.valueOf(sizeX));
size.add(IntTag.valueOf(sizeY));
size.add(IntTag.valueOf(sizeZ));
tag.put("size", size);
template.load(BuiltInRegistries.BLOCK, tag);
return new InlinePoolElement(template);
}
private static final class InlinePoolElement extends SinglePoolElement {
private final StructureTemplate inlineTemplate;
private InlinePoolElement(StructureTemplate template) {
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
inlineTemplate = template;
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return inlineTemplate.getBoundingBox(
new StructurePlaceSettings().setRotation(rotation), position);
}
}
private static final class BoundingPoolElement extends StructurePoolElement {
private final int width;
private final int depth;
private BoundingPoolElement(int width, int depth) {
super(StructureTemplatePool.Projection.RIGID);
this.width = width;
this.depth = depth;
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(width, 1, depth);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(
position.getX(), position.getY(), position.getZ(),
position.getX() + width - 1, position.getY(),
position.getZ() + depth - 1);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world,
StructureManager structureManager, ChunkGenerator chunkGenerator,
BlockPos position, BlockPos pivot, Rotation rotation,
BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
}
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
private BoundPoolHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
}
private void bind(StructureTemplatePool pool) {
bindValue(pool);
}
}
}
@@ -0,0 +1,45 @@
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;
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,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
frequency,
1,
Optional.empty(),
2,
1,
RandomSpreadType.LINEAR);
}
}
@@ -0,0 +1,199 @@
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;
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;
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.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;
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.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;
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.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;
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.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;
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;
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;
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;
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;
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;
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;
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;
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,108 @@
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.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.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();
}
@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,66 @@
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;
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_2_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_2_R1;
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;
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,149 @@
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;
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 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(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(biome, 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(biome, 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,511 @@
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;
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 net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import org.junit.Test;
import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.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 StructurePlacement.ExclusionZone(exclusionTarget, 1))),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
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,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new StructurePlacement.ExclusionZone(target, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> dependent = structureSetHolder(
"example:dependent",
dependentPlacement,
structureHolder("example:dependent"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:target")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
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,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
101,
Optional.of(new StructurePlacement.ExclusionZone(second, 1)),
32,
8,
RandomSpreadType.LINEAR),
"example:first"));
second.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
102,
Optional.of(new StructurePlacement.ExclusionZone(first, 1)),
27,
4,
RandomSpreadType.LINEAR),
"example:second"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:second")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
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,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new StructurePlacement.ExclusionZone(managedSet, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:allowed",
originalPlacement,
structureHolder("minecraft:village_plains"));
DatapackStructureScopeIndex index = index(
List.of("managed:blocked"),
List.of("managed:blocked"));
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> implements Holder<T> {
private final ResourceKey<T> key;
private final T value;
private KeyedHolder(ResourceKey<T> key, T value) {
this.key = key;
this.value = value;
}
@Override
public T value() {
return value;
}
@Override
public boolean isBound() {
return true;
}
@Override
public boolean areComponentsBound() {
return true;
}
@Override
public boolean is(Identifier identifier) {
return key.identifier().equals(identifier);
}
@Override
public boolean is(ResourceKey<T> candidate) {
return key.equals(candidate);
}
@Override
public boolean is(Predicate<ResourceKey<T>> predicate) {
return predicate.test(key);
}
@Override
public boolean is(TagKey<T> tag) {
return false;
}
@Override
public boolean is(Holder<T> holder) {
return holder == this;
}
@Override
public Stream<TagKey<T>> tags() {
return Stream.empty();
}
@Override
public DataComponentMap components() {
return DataComponentMap.EMPTY;
}
@Override
public Either<ResourceKey<T>, T> unwrap() {
return Either.left(key);
}
@Override
public Optional<ResourceKey<T>> unwrapKey() {
return Optional.of(key);
}
@Override
public Kind kind() {
return Kind.REFERENCE;
}
@Override
public boolean canSerializeIn(HolderOwner<T> owner) {
return true;
}
}
private static final class UnsupportedPlacement extends StructurePlacement {
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public StructurePlacementType<?> type() {
return null;
}
}
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
private MutableStructureSetHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -0,0 +1,252 @@
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;
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_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
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;
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_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;
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_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());
}
}
@@ -0,0 +1,96 @@
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;
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_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;
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,345 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class WorldgenTerrainHeightmapsTest {
private static final int MIN_Y = -64;
private static final int HEIGHT = 384;
private static final int LAND_TERRAIN_TOP = 70;
private static final int LAND_CANOPY_TOP = 80;
private static final int OCEAN_FLOOR_TOP = 60;
private static final int OCEAN_FLUID_TOP = 64;
private static final int LAND_X = 3;
private static final int LAND_Z = 5;
private static final int OCEAN_X = 8;
private static final int OCEAN_Z = 8;
private static final int TERRAIN_SLAB_DEPTH = 4;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void 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,294 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.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;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class VillageImporterListPoolElementRuntimeContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception {
StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown")
.apply(StructureTemplatePool.Projection.RIGID);
ListPoolElement towers = new ListPoolElement(
List.of(tower, overgrown),
StructureTemplatePool.Projection.RIGID
);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(towers));
Map<String, Object> entry = VillageImporter.piecePoolEntry(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
1
);
StructureLoss loss = VillageImporter.listElementFallbackLoss(
resolution,
"minecraft:pillager_outpost/towers"
);
Map<String, Object> basePlatePiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate",
List.of(Map.of("name", "minecraft:bottom")),
0
);
Map<String, Object> watchtowerPiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
List.of(),
1155
);
VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember(
"minecraft:pillager_outpost/features",
"qa/pillager_outpost/pool/minecraft/pillager_outpost/features",
false,
2,
"minecraft:empty",
"minecraft:pillager_outpost/feature_plate",
"qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate",
1,
0,
List.of(Map.of("name", "minecraft:bottom")));
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"));
assertEquals(1, entry.get("weight"));
assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability());
assertEquals("list_pool_overlays_not_imported", loss.code());
assertEquals(false, basePlatePiece.get("collidable"));
assertFalse(watchtowerPiece.containsKey("collidable"));
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition());
assertTrue(connectorScaffold.losses().isEmpty());
}
@Test
public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception {
StructurePoolElement waystone = StructurePoolElement
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(waystone));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"dungeons_and_taverns:waystones",
"qa/pool/dungeons_and_taverns/waystones",
false,
1,
"dungeons_and_taverns:waystones",
resolution.templateLocation(),
"qa/piece/dungeons_and_taverns/waystones/waystone_scaffold",
2,
0,
List.of());
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"));
assertEquals(2, normalization.poolEntry().get("weight"));
assertEquals("connectorless_all_air_member_normalized_empty",
normalization.losses().getFirst().code());
}
@Test
public void actualNonAirOrphanMemberIsOmitted() throws Exception {
StructurePoolElement orphan = StructurePoolElement
.legacy("test:orphan_house")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(orphan));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:orphan_pool",
"qa/pool/test/orphan_pool",
false,
1,
null,
resolution.templateLocation(),
"qa/piece/test/orphan_house",
4,
37,
List.of());
assertEquals(orphan.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals("test:orphan_house", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_non_air_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)"));
assertTrue(normalization.losses().getFirst().detail().contains("no source fallback"));
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualConnectorlessStartMemberRemainsPhysical() throws Exception {
StructurePoolElement start = StructurePoolElement
.legacy("test:air_start")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(start));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:start",
"qa/pool/test/start",
true,
2,
"test:fallback",
resolution.templateLocation(),
"qa/piece/test/air_start",
6,
0,
List.of());
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());
}
@Test
public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception {
StructurePoolElement inert = StructurePoolElement
.legacy("test:mixed_air_inert")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(inert));
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:mixed",
resolution.templateLocation(),
"qa/piece/test/mixed_air_inert",
9,
0,
List.of());
assertEquals(inert.getClass().getSimpleName(), resolution.physicalElement().typeName());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_all_air_mixed_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:mixed_all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:mixed_non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(allAirElement));
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(nonAirElement));
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/mixed_all_air_primary",
9,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/mixed_non_air_primary",
12,
41,
List.of());
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"));
assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
@Test
public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(allAirElement));
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(StructureReaderFixture.element(nonAirElement));
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/all_air_primary",
5,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/non_air_primary",
7,
29,
List.of());
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"));
assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
}
@@ -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;
}
}
@@ -0,0 +1,264 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
import ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
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.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LightChunk;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit;
import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class HeadlessChunkLightingTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void nativeLightCrossesChunkEdgesAndRoundTripsSavedNibblesWithoutServer() throws Exception {
Fixture fixture = new Fixture(-1, -2);
int edgeX = fixture.target.x() * 16;
int z = fixture.target.z() * 16 + 8;
for (ProtoChunk chunk : fixture.chunks) {
for (int x = 0; x < 16; x++) {
for (int localZ = 0; localZ < 16; localZ++) {
chunk.setBlockState(new BlockPos(chunk.getPos().getMinBlockX() + x, 0,
chunk.getPos().getMinBlockZ() + localZ), Blocks.STONE.defaultBlockState(), 0);
}
}
}
fixture.set(new BlockPos(edgeX - 1, 0, z), Blocks.AIR.defaultBlockState());
fixture.set(new BlockPos(edgeX - 1, -12, z), Blocks.GLOWSTONE.defaultBlockState());
fixture.finishFeatures();
assertNull(Bukkit.getServer());
HeadlessChunkLighting.light(fixture.request(true));
ProtoChunk target = fixture.chunk(fixture.target);
assertEquals(14, light(target, false, new BlockPos(edgeX, -12, z)));
assertEquals(13, light(target, false, new BlockPos(edgeX + 1, -12, z)));
assertEquals(14, light(target, true, new BlockPos(edgeX, -1, z)));
assertEquals(15, light(target, true, new BlockPos(edgeX, 1, z)));
assertEquals(0, light(target, true, new BlockPos(edgeX + 15, -1, z)));
assertEquals(ChunkStatus.LIGHT, target.getPersistedStatus());
assertTrue(target.isLightCorrect());
assertEquals(ChunkStatus.FEATURES, fixture.chunk(new ChunkPos(fixture.target.x() - 2, fixture.target.z())).getPersistedStatus());
assertFalse(fixture.chunk(new ChunkPos(fixture.target.x() - 2, fixture.target.z())).isLightCorrect());
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
temporary.newFolder().toPath()).createAccess("lighting")) {
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
HeadlessChunkSerialization.Context context = new HeadlessChunkSerialization.Context(
new StructurePieceSerializationContext(resources, fixture.registries, templates), fixture.containers, 0L);
CompoundTag tag = HeadlessChunkSerialization.copyOf(target, context).write();
assertEquals("minecraft:light", tag.getStringOr("Status", ""));
assertEquals(SaveUtil.STARLIGHT_LIGHT_VERSION, tag.getIntOr(SaveUtil.STARLIGHT_VERSION_TAG, -1));
assertTrue(tag.contains("isLightOn"));
assertFalse(tag.getBooleanOr("isLightOn", true));
assertThrows(ClassCastException.class, () -> SerializableChunkData.parse(target, fixture.containers, tag));
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
NbtIo.writeCompressed(tag, bytes);
CompoundTag restored = NbtIo.readCompressed(new ByteArrayInputStream(bytes.toByteArray()), NbtAccounter.unlimitedHeap());
assertEquals(tag, restored);
List<CompoundTag> sections = restored.getListOrEmpty("sections").compoundStream().toList();
assertEquals(target.getSectionsCount() + 2, sections.size());
for (CompoundTag section : sections) {
int index = section.getByteOr("Y", (byte) 0) - target.getMinSectionY() + 1;
assertTrue(section.contains(SaveUtil.BLOCKLIGHT_STATE_TAG));
assertTrue(section.contains(SaveUtil.SKYLIGHT_STATE_TAG));
SWMRNibbleArray block = new SWMRNibbleArray(section.getByteArray("BlockLight").orElse(null),
section.getIntOr(SaveUtil.BLOCKLIGHT_STATE_TAG, 0));
SWMRNibbleArray sky = new SWMRNibbleArray(section.getByteArray("SkyLight").orElse(null),
section.getIntOr(SaveUtil.SKYLIGHT_STATE_TAG, 0));
assertNibbleEquals(((StarlightChunk) target).starlight$getBlockNibbles()[index], block);
assertNibbleEquals(((StarlightChunk) target).starlight$getSkyNibbles()[index], sky);
}
}
assertNull(Bukkit.getServer());
}
@Test
public void nativeOpacityAndShapeRulesApplyAcrossTheBoundaryWithoutSkylight() {
for (BlockState barrier : List.of(Blocks.GLASS.defaultBlockState(), Blocks.TINTED_GLASS.defaultBlockState(),
Blocks.STONE_SLAB.defaultBlockState(), Blocks.STONE.defaultBlockState())) {
Fixture fixture = new Fixture(0, 0);
for (int x = -2; x <= 3; x++) {
for (int y = -13; y <= -11; y++) {
for (int z = 7; z <= 9; z++) {
fixture.set(new BlockPos(x, y, z), Blocks.STONE.defaultBlockState());
}
}
}
for (int x = -1; x <= 2; x++) {
fixture.set(new BlockPos(x, -12, 8), Blocks.AIR.defaultBlockState());
}
fixture.set(new BlockPos(-1, -12, 8), Blocks.GLOWSTONE.defaultBlockState());
fixture.set(new BlockPos(0, -12, 8), barrier);
fixture.finishFeatures();
HeadlessChunkLighting.light(fixture.request(false));
ProtoChunk target = fixture.chunk(fixture.target);
int expected = barrier.is(Blocks.GLASS) || barrier.is(Blocks.STONE_SLAB) ? 13 : 0;
assertEquals(barrier.toString(), expected, light(target, false, new BlockPos(1, -12, 8)));
assertEquals(0, light(target, true, new BlockPos(1, 20, 8)));
}
assertNull(Bukkit.getServer());
}
@Test
public void emptySectionsRetainImplicitSkyLightAcrossFullHeightAndPadding() {
for (boolean sky : List.of(true, false)) {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
HeadlessChunkLighting.light(fixture.request(sky));
ProtoChunk target = fixture.chunk(fixture.target);
for (int y = target.getMinY() - 16; y <= target.getMaxY() + 16; y++) {
assertEquals("sky y=" + y, sky ? 15 : 0, light(target, true, new BlockPos(8, y, 8)));
assertEquals("block y=" + y, 0, light(target, false, new BlockPos(8, y, 8)));
}
}
}
@Test
public void interruptionLeavesChunkUnlitAndRetainsInterrupt() {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
Thread.currentThread().interrupt();
try {
assertThrows(IllegalStateException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
assertTrue(Thread.currentThread().isInterrupted());
assertFalse(fixture.chunk(fixture.target).isLightCorrect());
assertEquals(ChunkStatus.FEATURES, fixture.chunk(fixture.target).getPersistedStatus());
} finally {
Thread.interrupted();
}
}
@Test
public void rejectsIncompleteNeighborsBeforePromotingOrMutatingLight() {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
fixture.chunks.removeFirst();
ProtoChunk target = fixture.chunk(fixture.target);
SWMRNibbleArray[] previous = ((StarlightChunk) target).starlight$getBlockNibbles();
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
assertEquals(ChunkStatus.FEATURES, target.getPersistedStatus());
assertFalse(target.isLightCorrect());
assertArrayEquals(previous, ((StarlightChunk) target).starlight$getBlockNibbles());
fixture.chunks.add(new ProtoChunk(new ChunkPos(-2, -2), UpgradeData.EMPTY, target, fixture.containers, null));
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
}
@Test
public void directStarlightRequiresLevelButNativeLightEnginesDoNot() {
Fixture fixture = new Fixture(0, 0);
ProtoChunk chunk = fixture.chunk(fixture.target);
LightChunkGetter source = new LightChunkGetter() {
@Override
public LightChunk getChunkForLighting(int x, int z) {
return chunk;
}
@Override
public BlockGetter getLevel() {
return chunk;
}
};
assertThrows(ClassCastException.class, () -> new StarLightInterface(source, true, true, null));
assertNull(Bukkit.getServer());
}
private static int light(ProtoChunk chunk, boolean sky, BlockPos position) {
StarlightChunk light = (StarlightChunk) chunk;
SWMRNibbleArray[] nibbles = sky ? light.starlight$getSkyNibbles() : light.starlight$getBlockNibbles();
return nibbles[(position.getY() >> 4) - chunk.getMinSectionY() + 1]
.getVisible(position.getX(), position.getY(), position.getZ());
}
private static void assertNibbleEquals(SWMRNibbleArray expected, SWMRNibbleArray actual) {
for (int index = 0; index < 4096; index++) {
assertEquals(expected.getVisible(index), actual.getVisible(index));
}
}
private static final class Fixture {
private final RegistryAccess registries = HeadlessNativeTestRegistries.get();
private final PalettedContainerFactory containers = PalettedContainerFactory.create(registries);
private final List<ProtoChunk> chunks = new ArrayList<>();
private final ChunkPos target;
private Fixture(int x, int z) {
target = new ChunkPos(x, z);
LevelHeightAccessor height = LevelHeightAccessor.create(-64, 128);
for (int dz = -2; dz <= 2; dz++) {
for (int dx = -2; dx <= 2; dx++) {
chunks.add(new ProtoChunk(new ChunkPos(x + dx, z + dz), UpgradeData.EMPTY, height, containers, null));
}
}
}
private ProtoChunk chunk(ChunkPos position) {
for (ProtoChunk chunk : chunks) {
if (chunk.getPos().equals(position)) {
return chunk;
}
}
throw new IllegalArgumentException("Missing fixture chunk " + position);
}
private void set(BlockPos position, BlockState state) {
chunk(new ChunkPos(position.getX() >> 4, position.getZ() >> 4)).setBlockState(position, state, 0);
}
private void finishFeatures() {
for (ProtoChunk chunk : chunks) {
chunk.setPersistedStatus(ChunkStatus.FEATURES);
}
}
private HeadlessChunkLighting.Request request(boolean sky) {
return new HeadlessChunkLighting.Request(chunks, Set.of(target), sky);
}
}
}
@@ -0,0 +1,161 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import net.minecraft.core.BlockPos;
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.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.util.RandomSource;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
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.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.ticks.ScheduledTick;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HeadlessChunkSerializationTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void snapshotMatchesNativeCopyOfIncludingStructuresLightAndReceipts() throws Exception {
RegistryAccess registries = HeadlessNativeTestRegistries.get();
PalettedContainerFactory containers = PalettedContainerFactory.create(registries);
LevelHeightAccessor height = LevelHeightAccessor.create(-64, 384);
ProtoChunk chunk = new ProtoChunk(new ChunkPos(-33, 17), UpgradeData.EMPTY, height, containers, null);
BlockPos block = new BlockPos(-528, -40, 272);
chunk.setBlockState(block, Blocks.CHEST.defaultBlockState(), 0);
CompoundTag chest = new CompoundTag();
chest.putString("id", "minecraft:chest");
chest.putInt("x", block.getX());
chest.putInt("y", block.getY());
chest.putInt("z", block.getZ());
chest.putString("LootTable", "minecraft:chests/simple_dungeon");
chunk.setBlockEntityNbt(chest);
CompoundTag entity = new CompoundTag();
entity.putString("id", "minecraft:pig");
chunk.addEntity(entity);
chunk.getBlockTicks().schedule(new ScheduledTick<>(Blocks.CHEST, block, 37L, 2L));
chunk.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER, block.above(), 41L, 3L));
chunk.addPackedPostProcess(new ShortArrayList(new short[]{17, 2049}), 1);
chunk.setInhabitedTime(192L);
Heightmap.primeHeightmaps(chunk, EnumSet.allOf(Heightmap.Types.class));
chunk.setPersistedStatus(ChunkStatus.TERRAIN);
chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"),
PersistentDataType.BYTE_ARRAY, new byte[]{1, -8, 92, 0});
StarlightChunk light = (StarlightChunk) chunk;
light.starlight$getBlockNibbles()[1].setFull();
light.starlight$getBlockNibbles()[1].updateVisible();
light.starlight$getSkyNibbles()[0].setFull();
light.starlight$getSkyNibbles()[0].updateVisible();
chunk.setLightCorrect(true);
Structure hut = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(
ResourceKey.create(Registries.STRUCTURE, Identifier.parse("minecraft:swamp_hut"))).value();
StructureStart start = new StructureStart(hut, chunk.getPos(), 2,
new PiecesContainer(List.of(new SwampHutPiece(RandomSource.create(69420L), block.getX(), block.getZ()))));
start.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 73L);
chunk.setAllStarts(Map.of(hut, start));
chunk.setAllReferences(Map.of(hut, new LongOpenHashSet(new long[]{ChunkPos.pack(-32, 17)})));
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
temporary.newFolder().toPath()).createAccess("serialization")) {
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
StructurePieceSerializationContext structures = new StructurePieceSerializationContext(resources, registries, templates);
HeadlessChunkSerialization.Context context = new HeadlessChunkSerialization.Context(structures, containers, 29L);
MinecraftServer server = mock(MinecraftServer.class);
when(server.getResourceManager()).thenReturn(resources);
when(server.registryAccess()).thenReturn(registries.freeze());
when(server.getStructureTemplateManager()).thenReturn(templates);
ServerLevel level = mock(ServerLevel.class);
when(level.getServer()).thenReturn(server);
when(level.registryAccess()).thenReturn(registries);
when(level.palettedContainerFactory()).thenReturn(containers);
when(level.getGameTime()).thenReturn(29L);
when(level.getMinSectionY()).thenReturn(height.getMinSectionY());
when(level.getMaxSectionY()).thenReturn(height.getMaxSectionY());
CompoundTag nativeTag = SerializableChunkData.copyOf(level, chunk).write();
SerializableChunkData snapshot = HeadlessChunkSerialization.copyOf(chunk, context);
CompoundTag headless = snapshot.write();
assertEquals(nativeTag, headless);
assertEquals("minecraft:terrain", headless.getStringOr("Status", ""));
assertEquals(29L, headless.getLongOr("LastUpdate", -1L));
assertEquals(192L, headless.getLongOr("InhabitedTime", -1L));
assertArrayEquals(new byte[]{1, -8, 92, 0}, headless.getCompoundOrEmpty("ChunkBukkitValues")
.getByteArray("iris:natural_terrain").orElseThrow());
assertFalse(headless.getListOrEmpty("block_entities").isEmpty());
assertFalse(headless.getListOrEmpty("block_ticks").isEmpty());
assertFalse(headless.getListOrEmpty("fluid_ticks").isEmpty());
assertTrue(headless.getCompoundOrEmpty("structures").getCompoundOrEmpty("starts").contains("minecraft:swamp_hut"));
CompoundTag serializedStart = headless.getCompoundOrEmpty("structures").getCompoundOrEmpty("starts")
.getCompoundOrEmpty("minecraft:swamp_hut");
StructureStart restored = StructureStart.loadStaticStart(structures, serializedStart, 69420L);
assertTrue(restored.isValid());
assertEquals(start.getBoundingBox(), restored.getBoundingBox());
assertEquals(73L, serializedStart.getCompoundOrEmpty("StructureBukkitValues")
.getLongOr("iris:structure_activation", -1L));
chunk.getSection(1).setBlockState(0, 8, 0, Blocks.AIR.defaultBlockState());
chunk.getPostProcessing()[1].clear();
chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE).getRawData()[0] = 0L;
assertEquals(headless, snapshot.write());
for (ChunkStatus status : List.of(ChunkStatus.EMPTY, ChunkStatus.STRUCTURE_STARTS,
ChunkStatus.STRUCTURE_REFERENCES, ChunkStatus.BIOMES, ChunkStatus.TERRAIN)) {
ProtoChunk fresh = new ProtoChunk(new ChunkPos(0, -1), UpgradeData.EMPTY, height, containers, null);
fresh.setPersistedStatus(status);
CompoundTag freshTag = HeadlessChunkSerialization.copyOf(fresh, context).write();
assertEquals(SerializableChunkData.copyOf(level, fresh).write(), freshTag);
assertEquals(status, SerializableChunkData.getChunkStatusFromTag(freshTag));
assertFalse(freshTag.contains("ChunkBukkitValues"));
assertTrue(freshTag.getListOrEmpty("entities").isEmpty());
assertTrue(freshTag.getListOrEmpty("block_entities").isEmpty());
}
chunk.setPersistedStatus(ChunkStatus.FULL);
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkSerialization.copyOf(chunk, context));
}
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.probe;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import net.minecraft.data.registries.VanillaRegistries;
import org.bukkit.Bukkit;
import org.junit.Test;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public final class HeadlessGenerationRegistryTest {
@Test
public void capturesGeneratedDefinitionsThroughNativeCodecs() throws Exception {
HeadlessNativeBootstrap.initialize();
String biome = HeadlessNativeTestRegistries.generatedBiome();
String dimension = HeadlessNativeTestRegistries.generatedDimension();
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
HeadlessNativeTestRegistries::get);
PlatformGenerationRegistry registry = platform.generationRegistry();
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
assertEquals(registry.canonicalDefinition("minecraft:dimension_type", "iris:native_test", dimension),
registry.generatedDefinition("minecraft:dimension_type", "iris:native_test"));
assertEquals(PlatformGenerationRegistry.Definition.resourceIdentity("minecraft:block", "minecraft:stone"),
registry.definition("minecraft:block", "minecraft:stone"));
assertThrows(IllegalStateException.class,
() -> registry.generatedDefinition("minecraft:worldgen/biome", "iris:absent"));
assertThrows(IllegalArgumentException.class,
() -> registry.canonicalDefinition("minecraft:worldgen/biome", "iris:invalid", "{}"));
assertNull(Bukkit.getServer());
}
@Test
public void rejectsContractCaptureBeforeCoherentRegistriesLoad() {
HeadlessNativeBootstrap.initialize();
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
VanillaRegistries::createWorldLookup);
assertThrows(IllegalStateException.class,
() -> platform.generationRegistry().definition("minecraft:block", "minecraft:stone"));
}
}
@@ -0,0 +1,90 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public final class HeadlessMantleInspection {
private HeadlessMantleInspection() {
}
public static void main(String[] arguments) throws Exception {
if (arguments.length != 6) {
throw new IllegalArgumentException("Expected pack, dimension, seed, world, regionX, regionZ");
}
Path temporary = Files.createTempDirectory("iris-mantle-inspection-");
Path copied = temporary.resolve("mantle");
try {
Process copy = new ProcessBuilder("cp", "-Rc", Path.of(arguments[3]).resolve("mantle-hydrology").toString(),
copied.toString()).inheritIO().start();
if (copy.waitFor() != 0) {
throw new IllegalStateException("Could not clone retained mantle for read-only inspection");
}
inspect(arguments, copied);
} finally {
try (Stream<Path> paths = Files.walk(temporary)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}
private static void inspect(String[] arguments, Path copied) throws Exception {
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
new File(arguments[0]), arguments[1], "[mantle-inspection]", runtime,
Path.of(System.getProperty("java.io.tmpdir"))));
RealPackProbeSupport.EngineSession session = workspace.openEngine(Long.parseLong(arguments[2]), false, "inspect")) {
Engine engine = session.engine();
Mantle<Matter> mantle = IrisEngineMantle.createMantle(engine, copied, engine::getData);
try {
AtomicInteger tiles = new AtomicInteger();
Map<String, Integer> materials = new TreeMap<>();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
int minimumX = Integer.parseInt(arguments[4]) << 5;
int minimumZ = Integer.parseInt(arguments[5]) << 5;
for (int z = minimumZ; z < minimumZ + 32; z++) {
for (int x = minimumX; x < minimumX + 32; x++) {
mantle.iterateChunk(x, z, TileWrapper.class, (localX, y, localZ, wrapper) -> {
TileData tile = wrapper.getData();
tiles.incrementAndGet();
materials.merge(tile.getMaterialKey(), 1, Integer::sum);
digest.update(encoded(tile));
});
}
}
System.out.println("retainedMantleTiles=" + tiles.get() + " materials=" + materials
+ " sha256=" + HexFormat.of().formatHex(digest.digest()));
} finally {
mantle.close();
}
}
}
private static byte[] encoded(TileData tile) {
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes)) {
tile.toBinary(output);
return bytes.toByteArray();
} catch (Exception failure) {
throw new IllegalStateException("Cannot serialize retained tile " + tile.getMaterialKey(), failure);
}
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockProperties;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
public final class HeadlessNativeBlockPredicateTest {
@BeforeClass
public static void bootstrap() {
HeadlessNativeBootstrap.initialize();
}
@Test
public void memoizedPredicatesMatchEveryNativeBlockStateIncludingStateProperties() {
for (Block block : BuiltInRegistries.BLOCK) {
for (BlockState state : block.getStateDefinition().getPossibleStates()) {
ModdedBlockState wrapped = ModdedBlockState.deferred(state, Map.of(), "iris:test");
for (int iteration = 0; iteration < 2; iteration++) {
assertEquals(NativeBlockProperties.isStorage(state), wrapped.isStorage());
assertEquals(NativeBlockProperties.isUpdatable(state), wrapped.isUpdatable());
}
}
}
}
@Test
public void repeatedWritesClassifyImmutableStateOnlyOnce() {
BlockState state = Blocks.CHEST.defaultBlockState();
ModdedBlockState wrapped = ModdedBlockState.deferred(state, Map.of(), "iris:chest");
boolean storage = NativeBlockProperties.isStorage(state);
boolean updatable = NativeBlockProperties.isUpdatable(state);
try (MockedStatic<NativeBlockProperties> properties = mockStatic(NativeBlockProperties.class, CALLS_REAL_METHODS)) {
for (int iteration = 0; iteration < 512; iteration++) {
assertEquals(storage, wrapped.isStorage());
assertEquals(updatable, wrapped.isUpdatable());
}
properties.verify(() -> NativeBlockProperties.isUpdatable(state), times(1));
properties.verify(() -> NativeBlockProperties.isStorage(state), times(2));
}
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.probe;
import net.minecraft.tags.BlockTags;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.Map;
import net.minecraft.world.level.block.Blocks;
import org.junit.ClassRule;
import org.junit.Test;
import org.bukkit.Bukkit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public final class HeadlessNativeBootstrapTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Test
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
for (Item item : BuiltInRegistries.ITEM) {
ItemStack stack = new ItemStack(item);
if (item != Items.AIR) {
assertTrue(stack.getMaxStackSize() > 0);
}
}
assertNotNull(new ItemStack(Items.APPLE).get(DataComponents.FOOD));
assertEquals(64, new ItemStack(Items.TRIAL_KEY).getMaxStackSize());
assertTrue(Bukkit.getServer() == null);
}
@Test
public void loadsActualNativeTagsWithoutStartingServer() throws Exception {
HeadlessNativeBootstrap.initialize();
assertTrue(Blocks.OAK_LOG.defaultBlockState().is(BlockTags.LOGS));
assertTrue(Blocks.OAK_LEAVES.defaultBlockState().is(BlockTags.LEAVES));
assertTrue(Blocks.STONE.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
assertFalse(Blocks.WATER.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
assertFalse(Blocks.SHORT_GRASS.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
assertTrue(Bukkit.getServer() == null);
}
}
@@ -0,0 +1,32 @@
package art.arcane.iris.probe;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.ClassRule;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public final class HeadlessNativeRegistriesTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Test
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
RegistryAccess registries = HeadlessNativeTestRegistries.get();
Structure swampHut = registries.lookupOrThrow(Registries.STRUCTURE)
.getValue(Identifier.parse("minecraft:swamp_hut"));
Holder<Biome> swamp = registries.lookupOrThrow(Registries.BIOME)
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:swamp")));
assertTrue(swampHut.biomes().contains(swamp));
Holder<Biome> custom = registries.lookupOrThrow(Registries.BIOME)
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse("iris:coherent_swamp")));
assertTrue(swampHut.biomes().contains(custom));
}
}
@@ -0,0 +1,59 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
import net.minecraft.core.registries.Registries;
import org.bukkit.Bukkit;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.Map;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
public final class HeadlessNativeRegistryTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@BeforeClass
public static void bootstrap() {
HeadlessNativeBootstrap.initialize();
}
@Test
public void corePlatformUsesNativeResolutionWithoutBiomeFallback() throws Exception {
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"), writer::registries);
assertEquals("headless-native-core", platform.platformName());
assertTrue(platform.block("minecraft:stone").isSolid());
assertFalse(platform.block("minecraft:short_grass").isSolid());
assertTrue(platform.block("minecraft:oak_log").isTreeBlock());
assertTrue(platform.biomeWriter().biomeIdFor("minecraft:plains") >= 0);
assertThrows(IllegalStateException.class, () -> platform.biomeWriter().biomeIdFor("iris:unknown"));
assertThrows(UnsupportedOperationException.class, platform::lootTableKeys);
}
@Test
public void resolvesNativeDynamicAndStaticRegistriesWithoutServer() throws Exception {
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(
writer::registries, writer::registries, message -> {
throw new AssertionError("Unavailable registry: " + message);
}));
assertNull(Bukkit.getServer());
assertEquals("iris:headless/test", access.biome("iris:headless/test").key());
assertNotNull(access.biome("minecraft:plains"));
assertNull(access.biome("iris:unknown"));
assertTrue(access.structureKeys().contains("minecraft:mineshaft"));
assertTrue(access.enchantmentKeys().contains("minecraft:fortune"));
assertEquals("minecraft:diamond_sword", access.item("DIAMOND SWORD").key());
assertEquals("creature", access.entity("minecraft:cow").spawnCategory());
assertTrue(access.blockStateProperties().containsKey("minecraft:oak_log"));
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
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.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
public final class HeadlessNativeStateTest {
@BeforeClass
public static void bootstrap() {
HeadlessNativeBootstrap.initialize();
}
@Test
public void platformAirResolvesOnceAndRetainsCanonicalNativeState() {
BlockState air = Blocks.AIR.defaultBlockState();
try (MockedStatic<ModdedBlockState> states = mockStatic(ModdedBlockState.class, CALLS_REAL_METHODS)) {
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
HeadlessNativeTestRegistries::get);
NativeBlockState wrapped = platform.air();
for (int iteration = 0; iteration < 1024; iteration++) {
assertSame(wrapped, platform.air());
assertSame(air, ((ModdedBlockState) platform.air()).handle());
}
states.verify(() -> ModdedBlockState.of(air, null), times(1));
}
}
@Test
public void nativeMaterialsDistinguishDecorationsFluidsGlassAndBlockEntities() {
assertFalse(NativeBlockResolver.strictParse("minecraft:short_grass").isSolid());
assertTrue(NativeBlockResolver.strictParse("minecraft:glass").isSolid());
assertFalse(NativeBlockResolver.strictParse("minecraft:glass").isOccluding());
assertTrue(NativeBlockResolver.strictParse("minecraft:stone").isOccluding());
assertTrue(NativeBlockResolver.strictParse("minecraft:oak_log").isTreeBlock());
assertTrue(NativeBlockResolver.strictParse("minecraft:oak_leaves").isTreeBlock());
assertTrue(NativeBlockResolver.strictParse("minecraft:chest").hasTileEntity());
NativeBlockState slab = NativeBlockResolver.strictParse("minecraft:oak_slab[waterlogged=true]");
assertTrue(slab.isWaterLogged());
assertFalse(slab.isFluid());
assertTrue(NativeBlockResolver.strictParse("minecraft:water").isFluid());
}
@Test
public void canonicalStateKeysRoundTripEveryNativeDefaultState() {
for (Block block : BuiltInRegistries.BLOCK) {
BlockState state = block.defaultBlockState();
ModdedBlockState decoded = NativeBlockResolver.strictParse(ModdedBlockState.serialize(state));
assertEquals(state, decoded.handle());
}
assertThrows(IllegalArgumentException.class,
() -> NativeBlockResolver.strictParse("minecraft:oak_log[axis=invalid]"));
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimensionType;
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
import art.arcane.iris.pack.datapack.IDataFixer;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
final class HeadlessNativeTestRegistries {
private static RegistryAccess registries;
private HeadlessNativeTestRegistries() {
}
static synchronized RegistryAccess get() {
if (registries != null) {
return registries;
}
HeadlessNativeBootstrap.initialize();
HolderLookup.Provider vanilla = VanillaRegistries.createWorldLookup();
String plains = biome(vanilla, "minecraft:plains");
String swamp = biome(vanilla, "minecraft:swamp");
Map<String, String> definitions = Map.of(
"data/iris/worldgen/biome/native_test.json", generatedBiome(),
"data/iris/dimension_type/native_test.json", generatedDimension(),
"data/iris/worldgen/biome/biomes/test.json", plains,
"data/iris/worldgen/biome/headless/test.json", plains,
"data/iris/worldgen/biome/coherent_swamp.json", swamp,
"data/minecraft/tags/worldgen/biome/has_structure/swamp_hut.json",
"{\"replace\":false,\"values\":[\"iris:coherent_swamp\"]}");
try (HeadlessNativeRegistries loaded = HeadlessNativeRegistries.load(definitions)) {
registries = loaded.registries();
return registries;
} catch (IOException failure) {
throw new UncheckedIOException(failure);
}
}
static String generatedBiome() {
return new IrisBiomeCustom().setId("native_test").generateJson(new DataFixerV263());
}
static String generatedDimension() {
return new IrisDimensionType(IDataFixer.Dimension.OVERWORLD,
new IrisDimensionTypeOptions(), 768, 768, -256).toJson(new DataFixerV263());
}
private static String biome(HolderLookup.Provider vanilla, String key) {
Biome biome = vanilla.lookupOrThrow(Registries.BIOME).getOrThrow(
ResourceKey.create(Registries.BIOME, Identifier.parse(key))).value();
return Biome.DIRECT_CODEC.encodeStart(vanilla.createSerializationContext(JsonOps.INSTANCE), biome)
.getOrThrow().toString();
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileData;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import java.util.Map;
import java.util.List;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.data.registries.VanillaRegistries;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public final class HeadlessNativeTileTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@BeforeClass
public static void bootstrap() {
HeadlessNativeBootstrap.initialize();
}
@Test
public void nativeTileBinaryRetainsTypedPayloadWithoutWorld() throws Exception {
NativeTileData captured = NativeTileData.capture("minecraft:player_head",
"{id:\"minecraft:skull\",profile:{id:[I;1,2,3,4]},note:[B;5b,6b]}",
message -> { throw new AssertionError(message); });
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
captured.toBinary(output);
}
NativeTileReader reader = new NativeTileReader(VanillaRegistries::createWorldLookup);
NativeTileData decoded;
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
decoded = reader.read(input);
}
CompoundTag payload = decoded.payload();
assertEquals(captured.payload(), payload);
assertArrayEquals(new int[]{1, 2, 3, 4}, payload.getCompoundOrEmpty("profile").getIntArray("id").orElseThrow());
assertArrayEquals(new byte[]{5, 6}, payload.getByteArray("note").orElseThrow());
assertEquals(captured, decoded);
}
@Test
public void writesNativeTilePayloadIntoChunkAndRejectsMismatchedBlocks() throws Exception {
ModdedTileData tile = ModdedTileData.capture("minecraft:chest",
"{LootTable:'minecraft:chests/simple_dungeon',LootTableSeed:17L}");
HeadlessNativeTiles.Placement placement = new HeadlessNativeTiles.Placement(2, 4, 6,
new BlockPos(-32, -64, 48), ModdedBlockState.of(Blocks.CHEST.defaultBlockState(), Map.of()), tile);
NativeRegionTerrainWriter.TileInput input = HeadlessNativeTiles.prepare(placement);
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
CompoundTag chunk = writer.encode(new NativeRegionTerrainWriter.TerrainInput(-2, 3, -64, 16,
(x, y, z) -> x == 2 && y == 4 && z == 6 ? Blocks.CHEST.defaultBlockState() : Blocks.AIR.defaultBlockState(),
(x, y, z) -> "minecraft:plains", List.of(input)));
CompoundTag entity = chunk.getListOrEmpty("block_entities").getCompoundOrEmpty(0);
assertEquals("minecraft:chest", entity.getStringOr("id", ""));
assertEquals(17L, entity.getLongOr("LootTableSeed", 0));
assertEquals(-60, entity.getIntOr("y", 0));
assertThrows(IllegalStateException.class, () -> HeadlessNativeTiles.prepare(new HeadlessNativeTiles.Placement(
2, 4, 6, placement.origin(), ModdedBlockState.of(Blocks.BARREL.defaultBlockState(), Map.of()), tile)));
}
}
@@ -0,0 +1,62 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import java.util.ArrayList;
import java.util.List;
final class HeadlessNativeTiles {
private HeadlessNativeTiles() {
}
static List<NativeRegionTerrainWriter.TileInput> capture(Engine engine,
RealPackProbeSupport.GeneratedChunk chunk,
BlockPos origin) {
List<NativeRegionTerrainWriter.TileInput> tiles = new ArrayList<>();
engine.getMantle().getMantle().iterateChunk(origin.getX() >> 4, origin.getZ() >> 4,
TileWrapper.class, (x, y, z, wrapper) -> tiles.add(prepare(new Placement(x, y, z, origin,
chunk.blocks().get(x, y, z), wrapper.getData()))));
return List.copyOf(tiles);
}
static NativeRegionTerrainWriter.TileInput prepare(Placement placement) {
if (!(placement.tile() instanceof ModdedTileData tile)
|| !(placement.state() instanceof ModdedBlockState original)) {
throw new IllegalStateException("Native tile export requires native block and tile data");
}
NativeTileData data = tile.nativeData();
BlockState state = data.adjustBlockState(original.handle());
BlockPos position = placement.origin().offset(placement.x(), placement.y(), placement.z());
if (!(state.getBlock() instanceof EntityBlock block)) {
throw new IllegalStateException("Tile data has no native block entity at " + position);
}
BlockEntity entity = block.newBlockEntity(position, state);
if (entity == null || !data.isApplicable(state, entity)) {
throw new IllegalStateException("Tile data does not match native block at " + position);
}
try {
CompoundTag payload = data.payload();
if (payload == null) {
throw new IllegalStateException("Tile data has no native payload at " + position);
}
return new NativeRegionTerrainWriter.TileInput(placement.x(), placement.y(), placement.z(),
ModdedBlockState.serialize(state), payload);
} catch (Exception failure) {
throw new IllegalStateException("Cannot export native tile payload at " + position, failure);
}
}
record Placement(int x, int y, int z, BlockPos origin, NativeBlockState state, TileData tile) {
}
}

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