Compare commits

...
Author SHA1 Message Date
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
2912 changed files with 306446 additions and 105932 deletions
-41
View File
@@ -1,41 +0,0 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (core, plugin, spi, probes)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run verification gates
run: ./gradlew :core:check :adapters:bukkit:plugin:test :adapters:bukkit:nms:v26_2_R1:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Fabric)
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Forge)
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (NeoForge)
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Test artifact verifiers
run: ./gradlew -p buildSrc test --console=plain --stacktrace
- name: Verify Bukkit artifact
run: ./gradlew verifyBukkitArtifact --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Verify modded artifacts
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
+5
View File
@@ -71,3 +71,8 @@ AGENTS.md
build-lt.sh
BUILDS/
.run/
# Version-content compat lane worktree. Generated, never source.
.compatwt/
.worktrees/
+34 -17
View File
@@ -14,7 +14,7 @@ Consider supporting development by buying Iris on Spigot.
## Language and localization
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/core/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
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.
## Platforms
@@ -64,22 +64,22 @@ content selecting on `#minecraft:is_overworld` and friends.
**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 shipping Overworld declares Towns & Towers
26.1 and Dungeons & Taverns 5.3.0. With the default automatic ingest enabled, the first restart
after download installs those external datapacks and leaves admission restart-required; complete
the ensuing clean restart so Minecraft loads them together with the Iris dimension types and
biomes. If automatic ingest is disabled, run `/iris datapack ingest restart=true` instead and
complete the restart it requests. Plain Spigot supports ordinary managed `/iris create`, but not
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.
**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. Before loading the shipping Overworld, manually place the exact
compatible Towns & Towers 26.1 and Dungeons & Taverns 5.3.0 archives in that save's `datapacks/`
directory; modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart
once only after the Iris pack and both external datapacks are present. Packs register their custom
dimension types, height ranges, biomes, and external structure keys during that boot; worlds created
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
@@ -184,6 +184,19 @@ returns `---`. A real zero returns `0`.
| `%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 |
@@ -197,7 +210,9 @@ returns `---`. A real zero returns `0`.
| `%iris_pregen.chunks-per-second%` | Current rate |
| `%iris_pregen.paused%` | `true` while the job is paused |
The world values are the surface reading at the player's block column. Walking refreshes them at most
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,
@@ -237,7 +252,7 @@ Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everyth
./gradlew buildAllToOut
```
builds every platform artifact into `dist/`:
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
```
Iris v<version> [CraftBukkit] <mc>.jar
@@ -250,9 +265,11 @@ Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `build
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a
consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo;
override with `-Plocation=/path/to/consumers`.
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
changes only the consumer directory. Both tasks copy all four verified platform jars into
`../PluginOuts/`.
If you need help compiling as a developer or contributor, ask in the Discord.
@@ -1,743 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftWorld;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
private static final int NOISE_BIOME_CACHE_MAX = 262144;
private static final int STRONGHOLD_RING_SEARCH_Y = 0;
private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112;
private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4;
private final long seed;
private final Engine engine;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
private volatile int cacheRuntimeId;
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
this.seed = seed;
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
this.cacheDimension = engine.getDimension();
this.cacheRuntimeId = engine.getCacheID();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
}
biomes.add(vanillaHolder);
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder == null) {
throw new IllegalStateException("Iris custom structure biome '"
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
}
biomes.add(customHolder);
}
}
}
if (biomes.isEmpty()) {
throw new IllegalStateException("Iris pack '" + engine.getName()
+ "' has no registered structure biomes");
}
return new ArrayList<>(biomes);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
if (o != null) {
return o;
}
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object fieldFor(Class<?> returns, Object in) {
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
private static Object invokeFor(Class<?> returns, Object in) {
for (Method i : in.getClass().getMethods()) {
if (i.getReturnType().equals(returns)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
for (Field i : sourceType.getDeclaredFields()) {
if (i.getType().equals(returnType)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
return null;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleStructureBiomes().stream();
}
Set<Holder<Biome>> possibleStructureBiomes() {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
if (lease == null) {
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
KMap<String, Holder<Biome>> m = new KMap<>();
if (customRegistry == null) {
return m;
}
for (IrisBiome i : engine.getAllBiomes()) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (holder == null) {
if (fallback != null) {
m.put(j.getId(), fallback);
}
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
continue;
}
m.put(j.getId(), holder);
}
}
}
return m;
}
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
if (customRegistry == null || registry == null) {
return Collections.unmodifiableMap(spawnBiomes);
}
for (IrisBiome irisBiome : engine.getAllBiomes()) {
if (!irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
return Collections.unmodifiableMap(spawnBiomes);
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
if (biome == null) {
return null;
}
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
return null;
}
ensureCachesCurrent();
return vanillaSpawnBiomes.get(biome.value());
}
}
private RegistryAccess registry() {
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
}
@Override
public Pair<BlockPos, Holder<Biome>> findBiomeHorizontal(
int x,
int y,
int z,
int searchRadius,
Predicate<Holder<Biome>> allowed,
RandomSource random,
Climate.Sampler sampler
) {
int quartStep = horizontalBiomeSearchQuartStep(y, searchRadius);
if (quartStep == 1) {
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
}
return super.findBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
}
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
? STRONGHOLD_RING_SEARCH_QUART_STEP
: 1;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within");
if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
}
}
return biomes;
}
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
return internalY > caveSwitchInternalY;
}
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
prepareVisibleBiomeBatch();
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
}
}
void prepareVisibleBiomeBatch() {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler
) {
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState
) {
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) {
return null;
}
try {
return engine.acquireGenerationLease(operation);
} catch (GenerationSessionException e) {
if (engine.isClosing() || e.isExpectedTeardown()) {
return null;
}
throw new IllegalStateException("Iris biome source could not acquire generation session for "
+ operation + ".", e);
}
}
private boolean isRuntimeAvailable() {
return !engine.isClosed() && engine.getComplex() != null;
}
private void ensureCachesCurrent() {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
}
synchronized (this) {
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
}
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
cacheRuntimeId = runtimeId;
}
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z);
}
Holder<Biome> holder = resolveBiomeHolder(
biomeRegistry, resolution.irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
}
return holder;
}
private Holder<Biome> resolveVisibleBiomeHolder(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
if (resolution == null) {
return getFallbackBiome();
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
org.bukkit.block.Biome vanillaBiome = resolution.underground
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
if (holder != null) {
return holder;
}
return getFallbackBiome();
}
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
if (holder != null) {
return holder;
}
}
return getFallbackBiome();
}
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null);
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
if (engine == null || engine.isClosed()) {
return null;
}
if (engine.getComplex() == null) {
return null;
}
int blockX = x << 2;
int blockZ = z << 2;
int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean underground = false;
IrisBiome irisBiome;
if (deepUnderground) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null) {
return null;
}
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
}
private BiomeResolution createBiomeResolution(
IrisBiome irisBiome,
boolean underground,
int blockX,
int blockY,
int blockZ
) {
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
}
private Holder<Biome> getFallbackBiome() {
if (fallbackBiome != null) {
return fallbackBiome;
}
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
if (holder != null) {
return holder;
}
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static long packColumnKey(int x, int z) {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
return null;
}
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
customBiomeId.toLowerCase(java.util.Locale.ROOT)
);
Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) {
return null;
}
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
return null;
}
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
if (optionalReferenceHolder.isEmpty()) {
return null;
}
return optionalReferenceHolder.get();
}
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(biomeKey);
if (identifier == null) {
return null;
}
return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null);
}
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
if (plains != null) {
return plains;
}
Holder<Biome> vanilla = firstHolder(registry);
if (vanilla != null) {
return vanilla;
}
return firstHolder(customRegistry);
}
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
for (Biome biome : registry) {
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
continue;
}
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
if (optionalHolder.isPresent()) {
return optionalHolder.get();
}
}
return null;
}
private static final class BiomeResolution {
private final IrisBiome irisBiome;
private final boolean underground;
private final int blockX;
private final int blockY;
private final int blockZ;
private final RNG rng;
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
this.irisBiome = irisBiome;
this.underground = underground;
this.blockX = blockX;
this.blockY = blockY;
this.blockZ = blockZ;
this.rng = rng;
}
}
}
@@ -1,8 +1,8 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.framework.NativeStructureFrequencyScale;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
@@ -1,12 +1,16 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedFeatureControl;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.decoration.IrisDecorationStep;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
@@ -26,6 +30,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
@@ -38,6 +43,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
@@ -55,9 +62,7 @@ final class ImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine;
private volatile FeatureTable featureTable;
private volatile IrisDimension inertDimension;
private volatile int settledRuntimeId;
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
ImportedFeatureStage(Engine engine) {
this.engine = engine;
@@ -69,7 +74,8 @@ final class ImportedFeatureStage {
* does today.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
FeatureTable table = featureTable;
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return null;
}
@@ -80,50 +86,46 @@ final class ImportedFeatureStage {
/**
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
*
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
*/
void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (settled(dimension, runtimeId)) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
synchronized (this) {
if (settled(dimension, runtimeId)) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
build(level, dimension);
settledRuntimeId = runtimeId;
runtimeStates.put(runtimeId, build(level, dimension));
}
}
private boolean settled(IrisDimension dimension, int runtimeId) {
if (settledRuntimeId != runtimeId) {
return false;
}
FeatureTable current = featureTable;
if (current != null && current.dimension() == dimension) {
return true;
}
return inertDimension == dimension;
void evictRuntime(int runtimeId) {
runtimeStates.remove(runtimeId);
}
private void build(WorldGenLevel level, IrisDimension dimension) {
private RuntimeFeatureState currentState() {
IrisDimension dimension = engine.getDimension();
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
return settled(state, dimension) ? state : null;
}
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
return state != null && state.dimension() == dimension;
}
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error);
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
if (!control.shouldGenerateFeatures()) {
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
FeatureTable built;
try {
@@ -131,23 +133,15 @@ final class ImportedFeatureStage {
} catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error);
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
if (built == null) {
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
featureTable = built;
inertDimension = null;
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps");
}
private void markInert(IrisDimension dimension) {
featureTable = null;
inertDimension = dimension;
return new RuntimeFeatureState(dimension, built);
}
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
@@ -196,8 +190,7 @@ final class ImportedFeatureStage {
*/
private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey());
@@ -211,7 +204,7 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, namespace + ":" + customBiome.getId());
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
}
}
return keys;
@@ -224,8 +217,7 @@ final class ImportedFeatureStage {
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
@@ -244,7 +236,10 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
derivatives.put(
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
derivative
);
}
}
return derivatives;
@@ -275,7 +270,8 @@ final class ImportedFeatureStage {
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
FeatureTable table = featureTable;
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return;
}
@@ -286,14 +282,31 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: NativeStructureWorldgenAccess.create(
level, centerPos,
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue;
}
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex);
}
} catch (Throwable error) {
@@ -344,12 +357,37 @@ final class ImportedFeatureStage {
return id == null ? feature.toString() : id.toString();
}
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
private Set<Holder<Biome>> chunkBiomes(
WorldGenLevel level,
SectionPos sectionPos,
FeatureTable table,
DimensionStackContext stackContext
) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) {
section.getBiomes().getAll(collected::add);
LevelChunkSection[] sections = neighbour.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (stackContext == null) {
section.getBiomes().getAll(collected::add);
continue;
}
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int quartY = 0; quartY < 4; quartY++) {
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
for (int quartX = 0; quartX < 4; quartX++) {
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
for (int quartZ = 0; quartZ < 4; quartZ++) {
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
blockX, internalY, blockZ);
if (layer.terrainContext().isSelfReferencing()) {
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
}
}
}
}
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -396,4 +434,7 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) {
}
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
}
}
@@ -1,18 +1,35 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeTransitionColumn;
import art.arcane.iris.nativegen.NativeTerrainHeightCache;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import java.util.function.LongPredicate;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineLifecycleTasks;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
import art.arcane.iris.world.history.SavedTerrainChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
@@ -30,10 +47,10 @@ import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.platform.reflect.WrappedField;
import art.arcane.iris.platform.reflect.WrappedReturningMethod;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
@@ -98,6 +115,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -106,7 +124,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
@@ -114,8 +132,16 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
public class IrisChunkGenerator extends CustomChunkGenerator {
public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate {
private static final Set<Heightmap.Types> AUTHORING_HEIGHTMAPS = Set.of(
Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG,
Heightmap.Types.WORLD_SURFACE,
Heightmap.Types.OCEAN_FLOOR,
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private static final Runnable NO_OP = () -> {
@@ -129,9 +155,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private final int runtimeHeight;
private final int runtimeSeaLevel;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile IrisDimension spawnTableDimension;
private volatile int spawnTableRuntimeId;
private final ImportedFeatureStage importedFeatures;
private final NativeTerrainHeightCache terrainHeights = new NativeTerrainHeightCache();
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
private volatile ReachableStructureCache reachableStructureCache;
private volatile StructureStepCache structureStepCache;
@@ -154,9 +179,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
}
installNativeStructureVolumeIndex(level);
}
private void evictRuntimeCaches(int runtimeId) {
terrainHeights.evictRuntime(runtimeId);
importedFeatures.evictRuntime(runtimeId);
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
ReachableStructureCache reachable = reachableStructureCache;
if (reachable != null && reachable.runtimeId() == runtimeId) {
reachableStructureCache = null;
}
StructureStepCache steps = structureStepCache;
if (steps != null && steps.runtimeId() == runtimeId) {
structureStepCache = null;
}
}
private void installNativeStructureVolumeIndex(ServerLevel level) {
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
@@ -182,7 +224,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
return null;
}
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
SectionPos.blockToSectionCoord(pos.getX()),
SectionPos.blockToSectionCoord(pos.getZ()),
"bukkit_nms_structure_locate");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
NativeStructureVanillaLocator.Candidate nativeCandidate =
@@ -361,9 +408,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
return;
}
ChunkPos chunkPos = access.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
@@ -380,6 +434,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
));
adjustGeneratedStructures(
registryAccess, access, previousStarts, configuredStarts, templateManager);
if (route != null) {
access.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"),
PersistentDataType.LONG, route.activation().activationId());
access.markUnsaved();
}
}
}
@@ -395,15 +454,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
if (configuredStarts.containsKey(structure)) {
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
BoundingBox footprint = start.getBoundingBox();
if (!engine.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
if (configuredStarts.containsKey(structure)) {
NativeStructureOwnershipStore.discard(
engine, structureId, chunkPos.x(), chunkPos.z());
}
continue;
}
if (configuredStarts.containsKey(structure)) {
continue;
}
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
@@ -428,7 +500,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -630,7 +702,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (runtimeLevel.getChunkSource().getGenerator() != this) {
return;
}
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences(
@@ -640,7 +716,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
customBiomeSource.prepareVisibleBiomeBatch();
@@ -655,31 +735,69 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_build_surface");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_build_surface");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
return;
}
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
}
}
@Override
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_apply_carvers");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_apply_carvers");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
return;
}
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
}
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
ichunkaccess.getPos(),
"bukkit_nms_chunk_pipeline");
GenerationSessionLease lease;
ChunkPos chunkPos = ichunkaccess.getPos();
BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_chunk_pipeline");
GenerationHistoryRuntimeRouter.RuntimeRoute route;
try {
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
} catch (RuntimeException | Error failure) {
stage.close();
throw failure;
}
GenerationSessionLease lease;
try {
CompletableFuture<ChunkAccess> pipeline = delegate
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
} catch (RuntimeException | Error failure) {
closeHistoryRoute(route, failure);
stage.close();
throw failure;
}
try {
CompletableFuture<ChunkAccess> delegatePipeline;
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
delegatePipeline = delegate.fillFromNoise(
blender, randomstate, structuremanager, ichunkaccess);
}
CompletableFuture<ChunkAccess> pipeline = delegatePipeline
.thenApply(filled -> {
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
openHistoryRuntimeScope(route);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
claimGeneratedSemantics(route, filled, engine.getMinHeight());
persistNaturalTerrain(filled, route);
primeWorldgenHeightmaps(filled);
return filled;
}
@@ -687,24 +805,95 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
pipeline.whenComplete((filled, failure) -> {
boolean cancelled = isCancellationFailure(failure);
lease.close();
stage.close();
if (failure == null) {
Throwable completionFailure = closeNoisePipelineResources(
failure, lease, route, stage);
if (completionFailure == null) {
completion.complete(filled);
} else if (cancelled) {
completion.cancel(false);
} else {
completion.completeExceptionally(failure);
completion.completeExceptionally(completionFailure);
}
});
lease.detachThread();
if (route != null) {
route.detachThread();
}
return completion;
} catch (RuntimeException | Error failure) {
lease.close();
closeHistoryRoute(route, failure);
stage.close();
throw failure;
}
}
private static void persistNaturalTerrain(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) {
if (route == null || route.naturalTerrain().isEmpty()) {
return;
}
try {
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"), PersistentDataType.BYTE_ARRAY,
NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId()));
} catch (IOException failure) {
throw new IllegalStateException("Unable to persist natural terrain for chunk " + chunk.getPos(), failure);
}
}
private static Throwable closeNoisePipelineResources(
Throwable failure,
GenerationSessionLease lease,
GenerationHistoryRuntimeRouter.RuntimeRoute route,
BukkitChunkGenerator.GenerationStagePermit stage
) {
Throwable result = failure;
try {
lease.close();
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
try {
if (route != null) {
route.close();
}
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
try {
stage.close();
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
return result;
}
private static void closeHistoryRoute(
GenerationHistoryRuntimeRouter.RuntimeRoute route,
Throwable failure
) {
if (route == null) {
return;
}
try {
route.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
}
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
if (failure == null) {
return closeFailure;
}
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
return failure;
}
private static boolean isCancellationFailure(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
@@ -714,9 +903,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
primeAuthoringHeightmaps(chunkAccess);
return;
}
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
}
static void primeAuthoringHeightmaps(ChunkAccess chunkAccess) {
Heightmap.primeHeightmaps(chunkAccess, AUTHORING_HEIGHTMAPS);
}
private IntBinaryOperator worldgenSurfaceHeight() {
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
}
@@ -725,9 +922,47 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
}
private IntBinaryOperator hostWorldgenSurfaceHeight() {
return (x, z) -> Engine.hostHeight(engine, x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator hostWorldgenFloorHeight() {
return (x, z) -> Engine.hostHeight(engine, x, z, true) + runtimeMinY + 1;
}
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
return EngineLifecycleTasks.call(engine, "bukkit_nms_mob_spawns",
() -> getAdmittedMobsAt(holder, structuremanager, enumcreaturetype, blockposition),
WeightedList.of(List.of()));
}
private WeightedList<MobSpawnSettings.SpawnerData> getAdmittedMobsAt(
Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
if (selection.mode() == NativeBiomeSpawnSelection.Mode.LOADING) {
return WeightedList.of(List.of());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns")) {
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
}
}
private WeightedList<MobSpawnSettings.SpawnerData> getMobsAtWithActiveRuntime(
Holder<Biome> holder,
StructureManager structuremanager,
MobCategory enumcreaturetype,
BlockPos blockposition,
NativeBiomeSpawnSelection selection
) {
Holder<Biome> vanillaSpawnBiome = switch (selection.mode()) {
case RETAINED -> customBiomeSource.getRetainedVanillaSpawnBiome(selection.derivativeKey());
case CURRENT -> customBiomeSource.getVanillaSpawnBiome(holder);
case NONE, LOADING -> null;
};
if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
}
@@ -747,18 +982,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return explicitSpawns;
}
IrisDimension spawnDimension = engine.getDimension();
int spawnRuntimeId = engine.getCacheID();
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
synchronized (mergedSpawnTables) {
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
mergedSpawnTables.clear();
spawnTableDimension = spawnDimension;
spawnTableRuntimeId = spawnRuntimeId;
}
}
}
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), vanillaSpawnBiome.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
}
@@ -774,19 +999,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean flatStudioTerrain = platformGenerator != null && platformGenerator.usesFlatStudioTerrain();
if (!flatStudioTerrain && !ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
}
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)
&& NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) {
if (!flatStudioTerrain) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
}
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
if (!flatStudioTerrain) {
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
}
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight());
}
}
@@ -838,6 +1078,15 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!isHistoricalStructureStart(world, start)
&& !engine.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
continue;
}
NativeStructureOwnershipRecord ownership =
NativeStructureOwnershipRecovery.resolve(
engine, world.getLevel(), structureId, structure, start);
@@ -867,25 +1116,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
world, chunkPos, heightmapStarts,
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
Predicate<BlockPos> protectedPosition = NativeGenerationWriteGuard.protectedPositions(
staticObjects, engine.getDimensionStackContext(), engine.getMinHeight());
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
protectedPosition);
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
boundedWorld, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
NativeStructureSurfaceFitter.SurfaceTerrainPlan surfaceTerrainPlan;
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, terrainTargets,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
surfaceTerrainPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
surfaceTerrainPlan.primeHeightmaps(chunk);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -893,7 +1151,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
boundedWorld, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain preparation", nativeStructureBatchContext(placementGroups),
@@ -903,7 +1161,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
@@ -913,7 +1171,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
world, area, vacuumFoundationPlan);
boundedWorld, area, surfaceTerrainPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
@@ -921,6 +1179,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private boolean isHistoricalStructureStart(WorldGenLevel world, StructureStart start) {
ChunkPos origin = start.getChunkPos();
if (!engine.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) {
return true;
}
ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false);
if (source == null) {
return false;
}
long activation = source.persistentDataContainer.getOrDefault(
new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 0L);
return NativeGenerationWriteGuard.isHistoricalStructure(engine, activation);
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
@@ -938,26 +1210,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight());
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
world,
chunkPos,
hostWorldgenSurfaceHeight(),
hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
NativeGenerationWriteGuard.protectedPositions(
engine.getDimension().getStaticObjectLayer(engine.getData()),
engine.getDimensionStackContext(), engine.getMinHeight()));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
} finally {
world.setCurrentlyGenerating(null);
}
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
int runtimeId = engine.getCacheID();
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures();
}
synchronized (this) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
@@ -972,7 +1252,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
return resolved;
}
}
@@ -1004,12 +1284,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
ChunkPos chunkPos = chunkAccess.getPos();
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
primeWorldgenHeightmaps(chunkAccess);
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return;
}
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
@@ -1035,12 +1321,47 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
return;
}
Holder<Biome> visibleBiome;
if (engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
}
}
@Override
public boolean test(long packedChunk) {
return !engine.isClosing() && !engine.isClosed()
&& engine.getComplex().allowsMantleChunkWrite(ChunkPos.getX(packedChunk), ChunkPos.getZ(packedChunk));
}
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
if (chunk.getPersistedStatus().isOrAfter(stage)) {
return false;
}
ChunkPos chunkPos = chunk.getPos();
return NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, stage)
|| engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
}
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
@@ -1073,22 +1394,68 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return platformGenerator.getAuthoringBaseHeight();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_height");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return levelheightaccessor.getMinY()
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
+ 1;
NativeTerrainHeightCache.Query query = new NativeTerrainHeightCache.Query(
engine.getCacheID(), i, j, heightmap_type,
levelheightaccessor.getMinY(), levelheightaccessor.getHeight());
OptionalInt resolved = terrainHeights.resolvedHeight(query,
() -> resolvedBaseHeight(i, j, heightmap_type, levelheightaccessor));
if (resolved.isPresent()) {
return resolved.getAsInt();
}
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = engine.getDimensionStackContext() == null
? engine.getHeight(i, j, ignoreFluid)
: Engine.hostHeight(engine, i, j, ignoreFluid);
return levelheightaccessor.getMinY() + height + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
}
private OptionalInt resolvedBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor) {
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
return resolved.isPresent()
? OptionalInt.of(NativeTransitionColumn.height(resolved.get(), type, heightAccessor))
: OptionalInt.empty();
}
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
int floorIndex = platformGenerator.getAuthoringFloorY() - levelheightaccessor.getMinY();
BlockState floor = platformGenerator.isJigsawStudioActive() && JigsawStudioGenerator.isLightFloor(i, j)
? Blocks.SMOOTH_STONE.defaultBlockState()
: Blocks.POLISHED_DEEPSLATE.defaultBlockState();
for (int index = 0; index < column.length; index++) {
column[index] = index == floorIndex
? floor
: Blocks.AIR.defaultBlockState();
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_column");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(i, j);
if (resolved.isPresent()) {
return NativeTransitionColumn.column(resolved.get(), levelheightaccessor);
}
boolean dimensionStack = engine.getDimensionStackContext() != null;
int block = dimensionStack
? Engine.hostHeight(engine, i, j, true)
: engine.getHeight(i, j, true);
int water = dimensionStack
? Engine.hostHeight(engine, i, j, false)
: engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) {
@@ -1113,25 +1480,79 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
int chunkX,
int chunkZ,
String operation
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) {
throw new IllegalStateException("Iris " + operation
+ " has no generation-history runtime router.");
}
return null;
}
try {
return router.openRoute(chunkX, chunkZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not route generation history for chunk "
+ chunkX + "," + chunkZ + ".", failure);
}
}
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
int blockX,
int blockZ,
String operation
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not route generation history at "
+ blockX + "," + blockZ + ".", failure);
}
}
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
GenerationHistoryRuntimeRouter.RuntimeRoute route
) {
return route == null ? null : route.openRuntimeScope();
}
private static void claimGeneratedSemantics(
GenerationHistoryRuntimeRouter.RuntimeRoute route,
ChunkAccess chunk,
int minimumY
) {
if (route == null) {
return;
}
try {
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
route.claimGeneratedSemantics((x, y, z) -> {
BlockState state = chunk.getBlockState(position.set(x, minimumY + y, z));
return state.isAir() || state.liquid();
});
} catch (IOException failure) {
throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure);
}
}
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
return platformGenerator == null
? BukkitChunkGenerator.GenerationStagePermit.noop()
: platformGenerator.acquireGenerationStage(operation);
}
private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage(
ChunkPos chunkPos,
String operation
) {
return platformGenerator == null
? BukkitChunkGenerator.GenerationStagePermit.noop()
: platformGenerator.acquireNoiseGenerationStage(
engine,
chunkPos.x(),
chunkPos.z(),
operation);
}
@Override
public Optional<Identifier> getTypeNameForDataFixer() {
return delegate.getTypeNameForDataFixer();
@@ -1184,13 +1605,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
private record SpawnTableKey(int runtimeId, Biome biome, Biome vanillaBiome, MobCategory category) {
}
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
private record StructureStepCache(int runtimeId, Registry<Structure> registry,
List<List<Structure>> structures) {
}
record StudioStructureState(
@@ -1,46 +1,47 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.history.SavedTerrainChunk;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.platform.bukkit.nms.DatapackStructureScopeResult;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.core.nms.MinecraftVersion;
import art.arcane.iris.core.nms.ServerShutdownBoundary;
import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.iris.platform.bukkit.nms.MinecraftVersion;
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
import art.arcane.iris.platform.bukkit.nms.container.BiomeColor;
import art.arcane.iris.platform.bukkit.nms.container.Pair;
import art.arcane.iris.platform.bukkit.nms.container.BlockProperty;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import art.arcane.iris.generation.cache.AtomicCache;
import art.arcane.iris.generation.chunk.TerrainChunk;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.nativegen.NativeStructureFactory;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import art.arcane.iris.util.project.agent.Agent;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.Vector3d;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.iris.world.storage.region.NBTWorld;
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAChunkBiomeContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAGlobalPalette;
@@ -51,14 +52,8 @@ import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
import art.arcane.volmlib.util.nbt.mca.palette.MCAPalettedContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.world.task.J;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
@@ -91,7 +86,6 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Block;
@@ -104,7 +98,6 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.LevelStem;
@@ -123,11 +116,11 @@ import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.TreeFeature;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Difficulty;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
@@ -155,10 +148,8 @@ import org.jetbrains.annotations.NotNull;
import java.awt.Color;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -169,8 +160,6 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class NMSBinding implements INMSBinding {
@@ -178,14 +167,21 @@ public class NMSBinding implements INMSBinding {
private volatile DataVersion dataVersion;
private final BlockData AIR = Material.AIR.createBlockData();
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer;
private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle();
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
@Override
public PlatformGenerationRegistry generationRegistry() {
return new NmsGenerationRegistry(
this::registry,
"bukkit-generation-registry-v1",
"bukkit-v26_2_R1-generated-registry-json-v1"
);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
@@ -824,6 +820,11 @@ public class NMSBinding implements INMSBinding {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public void invalidateNativeStructureVolumeIndex(Engine engine) {
NativeStructureVolumeIndex.invalidate(engine);
}
@Override
public int getBiomeId(Biome biome) {
for (World i : Bukkit.getWorlds()) {
@@ -1112,6 +1113,17 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public CompletableFuture<Void> flushSavedTerrainCapture(World world) {
return NmsSavedTerrainCapture.flush(world);
}
@Override
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(World world, int chunkX, int chunkZ,
int minimumY, int height) {
return NmsSavedTerrainCapture.capture(world, chunkX, chunkZ, minimumY, height);
}
@Override
public boolean saveAndUnloadChunk(World world, int x, int z) {
try {
@@ -1131,13 +1143,26 @@ public class NMSBinding implements INMSBinding {
}
@Override
public void flushChunkIO(World world) {
try {
ServerLevel level = ((CraftWorld) world).getHandle();
MoonriseRegionFileIO.flush(level);
} catch (Throwable e) {
IrisLogging.reportError(e);
public boolean pollChunkTask(World world) {
if (FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())) {
return false;
}
ServerLevel level = ((CraftWorld) world).getHandle();
if (Thread.currentThread() != level.getServer().getRunningThread()) {
throw new IllegalStateException("Native chunk tasks require the server lifecycle thread");
}
return level.getChunkSource().pollTask();
}
@Override
public void flushChunkIO(World world) {
ServerLevel level = ((CraftWorld) world).getHandle();
MoonriseRegionFileIO.flush(level);
}
@Override
public void reconcileNativeStructurePois(Chunk chunk) {
NativeStructurePoiUpdates.reconcile((LevelChunk) ((CraftChunk) chunk).getHandle(ChunkStatus.FULL));
}
@Override
@@ -1260,9 +1285,9 @@ public class NMSBinding implements INMSBinding {
: null;
boolean studioBootstrap = platformGenerator != null
&& platformGenerator.isStudioEntryBootstrapActive();
boolean jigsawStudio = platformGenerator != null
&& platformGenerator.isJigsawStudioActive();
if (jigsawStudio) {
boolean authoringStudio = platformGenerator != null
&& platformGenerator.isAuthoringStudio();
if (authoringStudio) {
requireIrisGenerator(generator);
if (currentState.possibleStructureSets().isEmpty()) {
currentState.ensureStructuresGenerated();
@@ -1415,7 +1440,14 @@ public class NMSBinding implements INMSBinding {
if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) {
return null;
}
return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason);
CraftWorld world = (CraftWorld) location.getWorld();
if (world.getDifficulty() == Difficulty.PEACEFUL) {
EntityType<?> nativeType = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString()));
if (nativeType == null || !nativeType.isAllowedInPeaceful()) {
return null;
}
}
return world.spawn(location, type.getEntityClass(), null, reason);
}
@Override
@@ -1533,113 +1565,37 @@ public class NMSBinding implements INMSBinding {
@Override
public boolean injectBukkit() {
synchronized (injected) {
if (injected.get()) {
return true;
}
try {
IrisLogging.info("Injecting Bukkit");
levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArguments(4))
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
.and(ElementMatchers.takesArgument(1, String.class))
.and(ElementMatchers.takesArgument(2, Path.class))
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
.installOn(Agent.getInstrumentation());
serverLevelTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
.make()
.load(clazz.getClassLoader(), Agent.installed());
}
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel,
partialStorageAccess
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable ignored) {
}
}
return false;
}
}
return worldLifecycle.injectBukkit();
}
@Override
public void ensureServerLevelInjection() {
if (!injected.get())
return;
try {
Agent.getInstrumentation().retransformClasses(
LevelStorageSource.LevelStorageAccess.class,
ServerLevel.class
);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to re-apply Bukkit world lifecycle injection");
}
worldLifecycle.ensureServerLevelInjection();
}
@Override
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
injected.set(false);
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
activeServerLevel,
activeStorageAccess
}) {
if (transformer == null) {
continue;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
}
}
}
worldLifecycle.uninjectBukkit();
}
@Override
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
return ServerShutdownBoundary.await(
() -> server.hasFullyShutdown,
server.getRunningThread(),
timeout,
unit
);
public boolean isServerStopping() {
return worldLifecycle.isServerStopping();
}
@Override
public ServerShutdownBoundary createServerShutdownBoundary() {
return worldLifecycle.createServerShutdownBoundary();
}
@Override
public void deferPluginClassLoaderClose() {
worldLifecycle.deferPluginClassLoaderClose();
}
@Override
public void releasePluginClassLoaderClose() {
worldLifecycle.releasePluginClassLoaderClose();
}
@Override
@@ -1685,187 +1641,4 @@ public class NMSBinding implements INMSBinding {
settings.updateLayers();
return new FlatLevelSource(settings);
}
private static class ChunkAccessAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
return index >= access.getPostProcessing().length;
}
}
private static class LevelStorageAccessAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(1) String levelId,
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
) {
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
return;
}
Object generator;
try {
generator = stagingType
.getDeclaredMethod("peekStemGenerator", String.class)
.invoke(null, levelId);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to inspect the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (generator == null || !generatorType.isInstance(generator)) {
return;
}
try {
Object target = generatorType.getMethod("getTarget").invoke(generator);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to bind the staged world storage identity",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
private static class ServerLevelAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.AllArguments Object[] constructorArguments
) {
if (dimensionKey == null)
return;
// This advice is inlined into every ServerLevel construction on the server. Until a
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
// Iris being unloaded or half-loaded must never break other plugins' world creation.
String levelId;
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
return;
}
// From here Iris is present and its classes resolve; a failure resolving the
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
ChunkGenerator constructorGenerator = null;
for (Object argument : constructorArguments) {
if (argument instanceof ChunkGenerator candidate) {
constructorGenerator = candidate;
break;
}
}
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = stagingType
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
gen = owned;
}
} catch (Throwable e) {
throw new RuntimeException("Iris failed to resolve the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (gen == null) {
return;
}
// Past the ownership gate the world is Iris-owned; silently handing back the
// vanilla stem would corrupt generation, so failures from here rethrow.
try {
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
if (bindings == null) {
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
}
java.lang.reflect.Method stemMethod = null;
for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) {
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
stemMethod = candidate;
break;
}
}
if (stemMethod == null) {
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
}
Object target = generatorType.getMethod("getTarget").invoke(gen);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
}
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
levelStem = runtimeStem;
} catch (Throwable e) {
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
}
@@ -0,0 +1,142 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
import java.util.Optional;
final class NativeStructurePoiUpdates {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
private final int skipPoiFlag;
NativeStructurePoiUpdates(int skipPoiFlag) {
this.skipPoiFlag = skipPoiFlag;
}
static NativeStructurePoiUpdates create() {
try {
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
if ((flags & skipPoiFlag) != 0) {
return world.setBlock(position, state, flags, limit);
}
ChunkAccess chunk = world.getChunk(position);
if (!(chunk instanceof ProtoChunk)) {
return world.setBlock(position, state, flags, limit);
}
if (!(chunk instanceof ImposterProtoChunk)
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
markSection(chunk, chunk.getSectionIndex(position.getY()));
}
return world.setBlock(position, state, flags | skipPoiFlag, limit);
}
static void reconcile(LevelChunk chunk) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
if (sections == null) {
return;
}
validateSections(chunk, sections);
ServerLevel world = (ServerLevel) chunk.getLevel();
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
PoiManager manager = world.getPoiManager();
for (int word = 0; word < sections.length; word++) {
long remaining = sections[word];
while (remaining != 0) {
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
reconcileSection(chunk, manager, index);
remaining &= remaining - 1;
}
}
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
chunk.markUnsaved();
}
private static void markSection(ChunkAccess chunk, int index) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
int word = index >>> 6;
long bit = 1L << (index & 63);
if (sections == null) {
sections = new long[word + 1];
} else {
validateSections(chunk, sections);
if (word < sections.length && (sections[word] & bit) != 0) {
return;
}
if (word >= sections.length) {
sections = Arrays.copyOf(sections, word + 1);
}
}
sections[word] |= bit;
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
}
private static void validateSections(ChunkAccess chunk, long[] sections) {
int count = chunk.getSectionsCount();
int words = (count + 63) >>> 6;
if (sections.length > words
|| sections.length == words && (count & 63) != 0
&& (sections[words - 1] >>> (count & 63)) != 0) {
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
}
}
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
if (existing.isPresent()) {
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
manager.remove(record.getPos());
}
}
}
LevelChunkSection section = chunk.getSection(index);
if (!section.maybeHas(PoiTypes::hasPoi)) {
return;
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
if (type.isEmpty()) {
continue;
}
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
sectionPosition.minBlockZ() + z);
if (!manager.getType(position).equals(type)) {
manager.add(position.immutable(), type.get());
}
}
}
}
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
@@ -11,6 +11,7 @@ import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
@@ -59,11 +60,14 @@ import java.util.function.Supplier;
final class NativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
private final WorldGenLevel delegate;
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
private final AABB generationBounds;
@@ -80,6 +84,9 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
@@ -99,9 +106,11 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY));
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
@@ -246,7 +255,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
@@ -359,13 +368,13 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null;
return isWritable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
@@ -424,17 +433,31 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
return isWritable(position)
&& (POI_UPDATES != null
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
: delegate.setBlock(position, state, updateFlags, updateLimit));
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
return isWritable(position) && delegate.removeBlock(position, move);
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
: delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? !delegate.getBlockState(position).isAir()
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
: delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
@@ -453,7 +476,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
}
private boolean isWritable(BlockPos position) {
return isReadable(position);
return isReadable(position) && !protectedPosition.test(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
@@ -544,7 +567,9 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
@@ -0,0 +1,61 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.agent.Agent;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import java.util.function.LongPredicate;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import java.util.List;
final class NmsGenerationHooks {
private NmsGenerationHooks() {
}
static void install() {
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
.make()
.load(clazz.getClassLoader(), Agent.installed());
}
buddy.redefine(WorldGenRegion.class)
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
.and(ElementMatchers.takesArguments(BlockPos.class))))
.make()
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
}
private static class WorldGenerationWriteAdvice {
@Advice.OnMethodExit
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
@Advice.Return(readOnly = false) boolean allowed) {
if (!allowed) {
return;
}
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
if (generator instanceof LongPredicate predicate
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator")
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
}
}
}
private static class ChunkAccessAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
return index >= access.getPostProcessing().length;
}
}
}
@@ -0,0 +1,182 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Supplier;
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
private static final String BLOCK_REGISTRY = "minecraft:block";
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
private final Supplier<RegistryAccess> registryAccess;
private final String runtimeIdentity;
private final String rendererIdentity;
NmsGenerationRegistry(
Supplier<RegistryAccess> registryAccess,
String runtimeIdentity,
String rendererIdentity
) {
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
case DIMENSION_TYPE_REGISTRY -> presence(
access,
Registries.DIMENSION_TYPE,
registryKey,
resourceKey
);
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
};
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
default -> throw new IllegalArgumentException(
"Registry does not accept generated JSON definitions: " + registryKey
);
};
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> exactRegistered(
access,
Registries.BIOME,
Biome.DIRECT_CODEC,
resourceKey
);
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
access,
Registries.DIMENSION_TYPE,
DimensionType.DIRECT_CODEC,
resourceKey
);
default -> throw new IllegalArgumentException(
"Registry does not contain Iris-generated definitions: " + registryKey
);
};
}
private static <T> Definition presence(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
String registryResourceKey,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
+ " / " + resourceKey + ".");
}
return Definition.resourceIdentity(registryResourceKey, resourceKey);
}
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static <T> Definition exactRegistered(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
Codec<T> codec,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing generated registry definition "
+ registryKey.identifier() + " / " + resourceKey + ".");
}
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static Identifier requireIdentifier(String resourceKey) {
Identifier identifier = Identifier.tryParse(resourceKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
}
return identifier;
}
private static String requireText(String value, String label) {
String required = Objects.requireNonNull(value, label).trim();
if (required.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return required;
}
}
@@ -0,0 +1,266 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.LockSupport;
final class NmsSavedTerrainCapture {
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
private static final int SNAPSHOT_BATCH_SIZE = 64;
private NmsSavedTerrainCapture() {
}
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
ServerLevel level = ((CraftWorld) world).getHandle();
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
J.runRegionFuture(world, chunkX, chunkZ, () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
if (chunk == null) {
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
return;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
}
snapshot.complete(scheduleSnapshot(level, chunk));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
return snapshot.thenApplyAsync(captured -> {
try {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
CompoundTag data = awaitSnapshot(level, captured, deadline);
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
awaitWrite(level, checkpoint.chunk(), deadline);
if (data == null) {
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
}
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
NbtIo.write(data, output);
}
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static CompletableFuture<Void> flush(World world) {
ServerLevel level = ((CraftWorld) world).getHandle();
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
return CompletableFuture.runAsync(() -> {
try {
flushSnapshots(world, level, holders);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
awaitSnapshots(snapshots, deadline);
for (CompletableFuture<CapturedChunk> future : snapshots) {
CapturedChunk captured = future.join();
CompoundTag data = awaitSnapshot(level, captured, deadline);
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
}
for (int index = offset; index < end; index++) {
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
}
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
}
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
World world, ServerLevel level, List<NewChunkHolder> holders) {
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
for (NewChunkHolder original : holders) {
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
snapshots.add(snapshot);
J.runRegionFuture(world, position.x(), position.z(), () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
.getChunkHolder(position.x(), position.z());
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
}
return snapshots;
}
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
throws IOException {
try {
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
}
}
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
for (Checkpoint checkpoint : checkpoints) {
awaitWrite(level, checkpoint.chunk(), deadline);
}
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
for (Checkpoint checkpoint : checkpoints) {
verifyCheckpoint(worldFolder, checkpoint);
}
}
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
if (data == null) {
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
}
byte[] receipt = data.getCompound("ChunkBukkitValues")
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
data.getStringOr("Status", ""), receipt, structureActivation));
}
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
Verification expected = checkpoint.verification();
if (expected == null) {
return;
}
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
expected.status(), expected.receipt(), expected.structureActivation());
}
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (true) {
checkWriteWait(chunk, deadlineNanos);
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
return;
}
helpSaveTasks(scheduler);
}
}
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
Completable<CompoundTag> nativeData = new Completable<>();
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
task.queue();
ChunkPos position = chunk.getPos();
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
return new CapturedChunk(position, data);
}
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (!captured.data().isDone()) {
checkWriteWait(captured.chunk(), deadlineNanos);
helpSaveTasks(scheduler);
}
try {
return captured.data().join();
} catch (CompletionException failure) {
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
}
}
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
CompletableFuture<CompoundTag> data) {
try {
CompoundTag serialized = snapshot.write();
nativeData.complete(serialized);
data.complete(serialized);
} catch (Throwable failure) {
nativeData.completeExceptionally(failure);
data.completeExceptionally(failure);
}
}
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
}
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
}
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
}
record Checkpoint(ChunkPos chunk, Verification verification) {
}
record Verification(String status, byte[] receipt, long structureActivation) {
}
}
@@ -0,0 +1,403 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.agent.Agent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
final class NmsWorldLifecycle {
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer;
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
private boolean pluginClassLoaderCloseDeferred;
public boolean injectBukkit() {
synchronized (injected) {
if (injected.get()) {
return true;
}
try {
IrisLogging.info("Injecting Bukkit");
Agent.requireClassLoaderCloseDeferral();
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
.getMethod("close").getDeclaringClass();
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
pluginClassLoaderTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
.with(loaderListener)
.type(ElementMatchers.is(loaderCloseType))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
.on(ElementMatchers.named("close")
.and(ElementMatchers.takesArguments(0))
.and(ElementMatchers.returns(void.class)))))
.installOn(Agent.getInstrumentation());
loaderListener.requireInstalled();
levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArguments(4))
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
.and(ElementMatchers.takesArgument(1, String.class))
.and(ElementMatchers.takesArgument(2, Path.class))
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
.installOn(Agent.getInstrumentation());
serverLevelTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
NmsGenerationHooks.install();
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
pluginClassLoaderTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel,
partialStorageAccess,
partialPluginClassLoader
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
}
}
return false;
}
}
}
public void ensureServerLevelInjection() {
if (!injected.get()) {
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
}
try {
Agent.getInstrumentation().retransformClasses(
LevelStorageSource.LevelStorageAccess.class,
ServerLevel.class
);
} catch (Throwable e) {
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
}
}
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
injected.set(false);
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
activeServerLevel,
activeStorageAccess
}) {
if (transformer == null) {
continue;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
}
}
}
}
public boolean isServerStopping() {
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
}
public ServerShutdownBoundary createServerShutdownBoundary() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
return new ServerShutdownBoundary(
() -> server.hasFullyShutdown,
server.getRunningThread()
);
}
public void deferPluginClassLoaderClose() {
synchronized (injected) {
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
return;
}
Agent.retainClassLoader(getClass().getClassLoader());
pluginClassLoaderCloseDeferred = true;
}
}
public void releasePluginClassLoaderClose() {
ResettableClassFileTransformer transformer;
boolean releaseLoader;
synchronized (injected) {
transformer = pluginClassLoaderTransformer;
pluginClassLoaderTransformer = null;
releaseLoader = pluginClassLoaderCloseDeferred;
pluginClassLoaderCloseDeferred = false;
}
try {
if (transformer != null
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
}
} finally {
if (releaseLoader) {
Agent.releaseClassLoader(getClass().getClassLoader());
}
}
}
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
private volatile boolean transformed;
private volatile Throwable failure;
@Override
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
boolean loaded, DynamicType dynamicType) {
transformed = true;
}
@Override
public void onError(String typeName, ClassLoader loader, JavaModule module,
boolean loaded, Throwable throwable) {
failure = throwable;
}
private void requireInstalled() {
if (!transformed || failure != null) {
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
}
}
}
private static class PluginClassLoaderCloseAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
Class<?> installer = Class.forName(
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
.invoke(null, loader));
}
}
private static class LevelStorageAccessAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(1) String levelId,
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
) {
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
return;
}
Object generator;
try {
generator = stagingType
.getDeclaredMethod("peekStemGenerator", String.class)
.invoke(null, levelId);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to inspect the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (generator == null || !generatorType.isInstance(generator)) {
return;
}
try {
Object target = generatorType.getMethod("getTarget").invoke(generator);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to bind the staged world storage identity",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
private static class ServerLevelAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.AllArguments Object[] constructorArguments
) {
if (dimensionKey == null)
return;
// This advice is inlined into every ServerLevel construction on the server. Until a
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
// Iris being unloaded or half-loaded must never break other plugins' world creation.
String levelId;
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
return;
}
// From here Iris is present and its classes resolve; a failure resolving the
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
ChunkGenerator constructorGenerator = null;
for (Object argument : constructorArguments) {
if (argument instanceof ChunkGenerator candidate) {
constructorGenerator = candidate;
break;
}
}
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = stagingType
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
gen = owned;
}
} catch (Throwable e) {
throw new RuntimeException("Iris failed to resolve the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (gen == null) {
return;
}
// Past the ownership gate the world is Iris-owned; silently handing back the
// vanilla stem would corrupt generation, so failures from here rethrow.
try {
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
if (bindings == null) {
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
}
Method stemMethod = null;
for (Method candidate : bindings.getClass().getMethods()) {
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
stemMethod = candidate;
break;
}
}
if (stemMethod == null) {
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
}
Object target = generatorType.getMethod("getTarget").invoke(gen);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
}
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
levelStem = runtimeStem;
} catch (Throwable e) {
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
import net.minecraft.core.Holder;
@@ -1,70 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest {
@Test
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("resolveBiomeHolder(registry, i.getStructureDerivativeKey())"));
assertTrue(source.contains("resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey())"));
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
}
@Test
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(source.contains("if (engine.isClosed())"));
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()"));
}
@Test
public void strongholdRingSearchSamplesOneQuartColumnPerChunk() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
assertTrue(source.contains(
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
}
@Test
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int lookupStart = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(");
int lookupEnd = source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", lookupStart);
assertTrue(lookupStart >= 0);
assertTrue(lookupEnd > lookupStart);
String lookup = source.substring(lookupStart, lookupEnd);
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
}
}
@@ -1,551 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
String locate = source.substring(locateStart, locateEnd);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
String reachability = source.substring(reachabilityStart, reachabilityEnd);
int bootstrapGate = locate.indexOf("!platformGenerator.shouldGenerateStructures()");
int generatorIdentity = locate.indexOf("level.getChunkSource().getGenerator() != this");
int lease = locate.indexOf("requireGenerationLease(\"bukkit_nms_structure_locate\")");
int nativePrediction = locate.indexOf("NativeStructureVanillaLocator.predict(");
assertTrue(locate.contains("reached its safety limit"));
assertTrue(locate.contains("unregistered structure holder"));
assertTrue(bootstrapGate >= 0);
assertTrue(generatorIdentity > bootstrapGate);
assertTrue(lease > generatorIdentity);
assertTrue(nativePrediction > lease);
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("IrisLogging.reportError"));
assertFalse(reachability.contains("catch (Throwable"));
assertFalse(reachability.contains("reachable = Set.of()"));
}
@Test
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
String placement = source.substring(placementStart, placementEnd);
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
assertTrue(adjustment.contains("\"vertical adjustment\""));
assertFalse(adjustment.contains("IrisLogging.reportError"));
assertTrue(placement.contains("\"resolution\""));
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"terrain preparation\""));
assertTrue(placement.contains("\"foundation repair\""));
assertFalse(placement.contains("\"terrain carving\""));
assertTrue(placement.contains("\"vegetation cleanup\""));
assertTrue(placement.contains("\"placement\""));
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
@Test
public void heightmapRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_heightmaps\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_height\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
}
@Test
public void structureReferenceRepairIsGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int referencesStart = source.indexOf("public void createReferences");
int referencesEnd = source.indexOf("public CompletableFuture<ChunkAccess> createBiomes", referencesStart);
String references = source.substring(referencesStart, referencesEnd);
int nativeStructureGate = references.indexOf("!platformGenerator.shouldGenerateStructures()");
int generatorIdentity = references.indexOf("runtimeLevel.getChunkSource().getGenerator() != this");
int stage = references.indexOf("requireGenerationStage(\"bukkit_nms_create_references\")");
int lease = references.indexOf("requireGenerationLease(\"bukkit_nms_create_references\")");
int repair = references.indexOf("NativeStructureReferenceRepair.createReferences(");
assertTrue(nativeStructureGate >= 0);
assertTrue(generatorIdentity > nativeStructureGate);
assertTrue(stage > generatorIdentity);
assertTrue(lease > stage);
assertTrue(repair > lease);
assertTrue(references.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertFalse(references.contains("delegate.createReferences("));
assertFalse(references.contains("catch ("));
}
@Test
public void structureGenerationAcquiresBootstrapGateBeforeUsingPublishedState() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int generationStart = source.indexOf("public void createStructures(");
int generationEnd = source.indexOf("private void adjustGeneratedStructures", generationStart);
String generation = source.substring(generationStart, generationEnd);
int gate = generation.indexOf("!platformGenerator.shouldGenerateStructures()");
int currentGenerator = generation.indexOf(
"runtimeLevel.getChunkSource().getGenerator() != this");
int stateIdentity = generation.indexOf(
"runtimeLevel.getChunkSource().getGeneratorState() != structureState");
int stage = generation.indexOf(
"requireGenerationStage(\"bukkit_nms_create_structures\")");
int lease = generation.indexOf(
"requireGenerationLease(\"bukkit_nms_create_structures\")");
int generationCall = generation.indexOf(
"super.createStructures(registryAccess, structureState");
assertTrue(gate >= 0);
assertTrue(currentGenerator > gate);
assertTrue(stateIdentity > currentGenerator);
assertTrue(stage > stateIdentity);
assertTrue(lease > stage);
assertTrue(generationCall > lease);
assertTrue(generation.contains("structureState,\n structureManager"));
assertFalse(generation.contains("resolvePublishedStructureState"));
}
@Test
public void nativeRingBootstrapTracksTheExactMinecraftFutures() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int initializeStart = source.indexOf("private CompletableFuture<Void> initializeStructureState(");
int initializeEnd = source.indexOf("private Map<?, ?> structureRingPositions", initializeStart);
String initialize = source.substring(initializeStart, initializeEnd);
int completionStart = source.indexOf(
"private CompletableFuture<Void> structureRingCompletion", initializeEnd);
int completionEnd = source.indexOf("private void requireCurrentStructureOwner", completionStart);
String completion = source.substring(completionStart, completionEnd);
assertBefore(initialize,
"structureRingPositions(structureState)",
"structureState.ensureStructuresGenerated()");
assertBefore(initialize,
"structureState.ensureStructuresGenerated()",
"structureRingCompletion(ringPositions)");
assertFalse(initialize.contains("catch ("));
assertTrue(completion.contains("for (Object candidate : ringPositions.values())"));
assertTrue(completion.contains("CompletableFuture.allOf(completions)"));
assertTrue(completion.contains("throw new IllegalStateException("));
assertFalse(completion.contains("join()"));
assertFalse(completion.contains("get()"));
}
@Test
public void standardActivationClaimsTheExactPublishedStateBeforeInitialization() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int activationStart = source.indexOf("CompletableFuture<Void> activateStudioStructureState(");
int activationEnd = source.indexOf(
"private CompletableFuture<Void> startStructureStateBootstrap", activationStart);
String activation = source.substring(activationStart, activationEnd);
int bootstrapStart = activationEnd;
int bootstrapEnd = source.indexOf(
"private CompletableFuture<Void> initializeStructureState", bootstrapStart);
String bootstrap = source.substring(bootstrapStart, bootstrapEnd);
assertTrue(activation.contains("retained.structureState()"));
assertBefore(activation,
"retainedStudioStructureState(",
"claimStudioStructureState(retained)");
assertBefore(bootstrap,
"claim,",
"initializeStructureState(");
assertBefore(bootstrap,
"initializeStructureState(",
"activation.run()");
assertFalse(activation.contains("publishStructureState("));
assertFalse(source.contains("ChunkGeneratorStructureState fullState"));
}
@Test
public void terrainWritesPrimeTheWorldgenHeightmapsForEveryChunk() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
int fillEnd = source.indexOf("public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt", fillStart);
String fill = source.substring(fillStart, fillEnd);
int decorationStart = source.indexOf("public void addVanillaDecorations");
int decorationEnd = source.indexOf("public void spawnOriginalMobs", decorationStart);
String decorations = source.substring(decorationStart, decorationEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(fill.contains("thenApply"));
assertTrue(fill.contains("primeWorldgenHeightmaps"));
assertTrue(decorations.contains("primeWorldgenHeightmaps"));
assertFalse(decorations.contains("Heightmap.Types.WORLD_SURFACE_WG"));
assertFalse(decorations.contains("Heightmap.Types.OCEAN_FLOOR_WG"));
assertEquals(1, occurrences(source, "WorldgenTerrainHeightmaps.primeTerrain("));
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(fill.contains("requireGenerationLease(\"bukkit_nms_chunk_pipeline\")"));
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@Test
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
.replace("\r\n", "\n");
String createBiomes = method(
source,
"public CompletableFuture<ChunkAccess> createBiomes",
"public void buildSurface");
String buildSurface = method(
source,
"public void buildSurface",
"public void applyCarvers");
String carvers = method(
source,
"public void applyCarvers",
"public CompletableFuture<ChunkAccess> fillFromNoise");
String noise = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
String decoration = method(
source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
String spawning = method(
source,
"public void spawnOriginalMobs",
"private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables");
String baseHeight = method(
source,
"public int getBaseHeight",
"public NoiseColumn getBaseColumn");
String baseColumn = method(
source,
"public NoiseColumn getBaseColumn",
"private GenerationSessionLease requireGenerationLease");
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
assertTrue(createBiomes.contains("sampler, resolverState"));
assertTrue(buildSurface.contains("delegate.buildSurface("));
assertTrue(carvers.contains("delegate.applyCarvers("));
assertTrue(noise.contains("requireNoiseGenerationStage("));
assertTrue(noise.contains("\"bukkit_nms_chunk_pipeline\""));
assertTrue(noise.contains("stage.close()"));
assertTrue(noise.contains("completion.cancel(false)"));
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
assertTrue(spawning.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(baseHeight.contains("engine.getHeight("));
assertTrue(baseColumn.contains("engine.getHeight("));
assertFalse(source.contains("synthetic_entry"));
assertFalse(source.contains("StudioEntryChunkGenerator"));
}
@Test
public void fillFromNoiseLeaseSpansTheDelegateAndHeightmapPipeline() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
int fillEnd = source.indexOf("private static boolean isCancellationFailure", fillStart);
String fill = source.substring(fillStart, fillEnd);
int productionStart = fill.indexOf(
" BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
0);
assertTrue(productionStart >= 0);
String productionFill = fill.substring(productionStart);
int completionStart = productionFill.indexOf("pipeline.whenComplete(");
int completionEnd = productionFill.indexOf(" return completion;", completionStart);
assertTrue(completionStart >= 0);
assertTrue(completionEnd > completionStart);
String completion = productionFill.substring(completionStart, completionEnd);
assertBefore(productionFill,
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
"GenerationSessionLease lease");
assertBefore(productionFill,
"requireNoiseGenerationStage(",
"\"bukkit_nms_chunk_pipeline\")");
assertBefore(productionFill,
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
assertBefore(completion, "lease.close();", "stage.close();");
assertBefore(completion, "stage.close();", "completion.complete(filled)");
assertBefore(completion, "stage.close();", "completion.cancel(false)");
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
assertTrue(completion.contains("else if (cancelled)"));
assertFalse(completion.contains("pipeline.isCancelled()"));
assertFalse(completion.contains("finally"));
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
assertTrue(productionFill.contains("return completion;"));
assertFalse(productionFill.contains("return pipeline;"));
assertFalse(productionFill.contains("pipeline.cancel("));
assertFalse(productionFill.contains("bukkit_nms_worldgen_heightmaps"));
}
@Test
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
Semaphore admission = new Semaphore(1, true);
admission.acquire();
CountDownLatch exclusiveEntered = new CountDownLatch(1);
CountDownLatch releaseExclusive = new CountDownLatch(1);
AtomicBoolean dependentFinished = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
boolean stageReleased = false;
try {
Future<?> exclusive = executor.submit(() -> {
boolean acquired = false;
try {
admission.acquire();
acquired = true;
exclusiveEntered.countDown();
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} finally {
if (acquired) {
admission.release();
}
}
});
awaitQueueLength(admission, 1);
CompletableFuture<Void> outward = new CompletableFuture<>();
outward.thenRun(() -> {
try {
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
dependentFinished.set(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
});
admission.release();
stageReleased = true;
assertTrue(outward.complete(null));
assertTrue(dependentFinished.get());
releaseExclusive.countDown();
exclusive.get(2, TimeUnit.SECONDS);
assertEquals(1, admission.availablePermits());
} finally {
if (!stageReleased) {
admission.release();
}
releaseExclusive.countDown();
executor.shutdownNow();
}
}
@Test
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() throws IOException {
CompletableFuture<Void> delegate = new CompletableFuture<>();
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
assertTrue(delegate.cancel(false));
assertFalse(transformed.isCancelled());
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
assertTrue(failure.getCause() instanceof CancellationException);
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String cancellation = method(
source,
"private static boolean isCancellationFailure",
"private void primeWorldgenHeightmaps");
assertTrue(cancellation.contains("current instanceof CompletionException"));
assertTrue(cancellation.contains("current instanceof CancellationException"));
}
@Test
public void noiseAdmissionCanPrepareStudioBeforeAcquiringTheGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String fill = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
String admission = method(
source,
"private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage",
"public Optional<Identifier> getTypeNameForDataFixer");
assertBefore(fill,
"requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
assertTrue(admission.contains("platformGenerator.acquireNoiseGenerationStage("));
assertBefore(admission, "engine,", "chunkPos.x(),");
assertBefore(admission, "chunkPos.x(),", "chunkPos.z(),");
assertFalse(fill.contains("requireGenerationStage(\"bukkit_nms_chunk_pipeline\")"));
}
@Test
public void everyTopLevelMoonriseStageEntersTheFairGateBeforeItsGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
String decoration = method(source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
assertStageBeforeLease(references, "bukkit_nms_create_references");
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
assertBefore(noise,
"requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
assertStageBeforeLease(decoration, "bukkit_nms_biome_decoration");
assertBefore(decoration,
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
"importedFeatures.prepare(generatoraccessseed)");
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
assertFalse(source.contains("GenerationSessionManager"));
}
@Test
public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java");
assertTrue("Worldgen heightmap priming must be shared with the modded loaders through "
+ nativegen, Files.isRegularFile(shared));
String heightmaps = Files.readString(shared).replace("\r\n", "\n");
assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;"));
assertTrue(heightmaps.contains("public static void primeTerrain("));
assertTrue(heightmaps.contains("public static void primeStructurePlacement("));
assertFalse(heightmaps.contains("org.bukkit"));
assertFalse(heightmaps.contains("craftbukkit"));
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
}
@Test
public void onlySidecarOwnedInjectedStartsUsePersistedPlacementPolicy() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
int ownershipResolution = placement.indexOf(
"NativeStructureOwnershipRecovery.resolve(");
int persistedDecision = placement.indexOf("ownership.restoredDecision()", ownershipResolution);
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
String factory = Files.readString(nativegen.resolve("NativeStructureFactory.java")).replace("\r\n", "\n");
String injector = Files.readString(nativegen.resolve("NativeStructureStartInjector.java")).replace("\r\n", "\n");
String recovery = Files.readString(nativegen.resolve("NativeStructureOwnershipRecovery.java")).replace("\r\n", "\n");
int ownershipRecord = injector.indexOf("NativeStructureOwnershipStore.record(");
int startPublication = injector.indexOf(
"context.structureManager().setStartForStructure(", ownershipRecord);
assertTrue(ownershipResolution >= 0);
assertTrue(persistedDecision > ownershipResolution);
assertTrue(recovery.contains("NativeStructureOwnershipStore.findPersisted("));
assertTrue(recovery.contains("NativeStructureOwnershipStore.record("));
assertTrue(ownershipRecord >= 0);
assertTrue(startPublication > ownershipRecord);
assertTrue(factory.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
assertTrue(adjustment.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
assertFalse(source.contains("wrapManaged("));
assertFalse(source.contains("isIrisManagedStart("));
}
private static int occurrences(String source, String needle) {
int count = 0;
int index = source.indexOf(needle);
while (index >= 0) {
count++;
index = source.indexOf(needle, index + needle.length());
}
return count;
}
private static void awaitQueueLength(Semaphore semaphore, int expected) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertTrue("Expected at least " + expected + " queued semaphore threads",
semaphore.getQueueLength() >= expected);
}
private static String method(String source, String startToken, String endToken) {
int start = source.indexOf(startToken);
int end = source.indexOf(endToken, start);
assertTrue("Missing source contract token: " + startToken, start >= 0);
assertTrue("Missing source contract token: " + endToken, end > start);
return source.substring(start, end);
}
private static void assertStageBeforeLease(String source, String operation) {
assertBefore(source,
"requireGenerationStage(\"" + operation + "\")",
"requireGenerationLease(\"" + operation + "\")");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
@Test
public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int spawnStart = source.indexOf("public void spawnOriginalMobs");
int spawnEnd = source.indexOf("private static WeightedList", spawnStart);
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))"));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
}
}
@@ -1,190 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void nativeLocateAllowsMonumentsAfterPolicyAndReachabilityChecks() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
assertTrue(irisHelperStart > findStart);
assertTrue(filterStart > irisHelperStart);
String outerMethod = source.substring(findStart, irisHelperStart);
String irisHelper = source.substring(irisHelperStart, filterStart);
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int placedCheck = irisHelper.indexOf(
"if (!IrisStructureLocator.hasNativePlacement(engine, structureId))");
int irisLocate = irisHelper.indexOf("NativeStructureLocatePersistence.search(", placedCheck);
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
int nativePrediction = outerMethod.indexOf("NativeStructureVanillaLocator.predict(", nativeFilter);
int irisResolution = outerMethod.indexOf("return findNearestIrisStructure(", nativePrediction);
int nearestSelection = irisHelper.indexOf(
"NativeStructureLocateResults.nearest(pos, predicted, nativeLocated)");
int selectedVerification = irisHelper.indexOf("bestSearch.search().verify(bestResult)", nearestSelection);
int selectedReference = irisHelper.indexOf(
"selectedSearch.search().reference(selectedStart)", selectedVerification);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int policyFilter = filterMethod.indexOf("if (!decision.generate())");
int filterContinue = filterMethod.indexOf("continue;", policyFilter);
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue);
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
assertTrue(registryLookup >= 0);
assertTrue(placedCheck > registryLookup);
assertTrue(irisLocate > placedCheck);
assertTrue(searchLimit > irisLocate);
assertTrue(nativeFilter >= 0);
assertTrue(nativePrediction > nativeFilter);
assertTrue(irisResolution > nativePrediction);
assertTrue(nearestSelection > irisLocate);
assertTrue(selectedVerification > nearestSelection);
assertTrue(selectedReference > selectedVerification);
assertTrue(policyFilter >= 0);
assertTrue(filterContinue > policyFilter);
assertTrue(emptyNativePartition > filterContinue);
assertTrue(reachabilityLookup > emptyNativePartition);
assertFalse(filterMethod.contains("NativeStructureLocateCapability"));
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
assertTrue(irisHelper.contains("NativeStructureLocatePersistence.probe("));
assertTrue(irisHelper.contains("NativeStructureLocateResults.selectAndReference("));
assertTrue(irisHelper.contains("selectedSearch.search().reference(selectedStart)"));
assertTrue(irisHelper.contains("findUnexplored"));
assertTrue(irisHelper.contains("verified.ownership().locatorY()"));
assertFalse(outerMethod.contains("delegate.findNearestMapStructure("));
}
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
@Test
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
AtomicInteger irisReferences = new AtomicInteger();
AtomicInteger nativeReferences = new AtomicInteger();
Pair<BlockPos, Holder<Structure>> irisSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeFar, () -> nativeReferences.incrementAndGet());
assertSame(irisNear, irisSelected);
assertEquals(1, irisReferences.get());
assertEquals(0, nativeReferences.get());
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeNear, () -> nativeReferences.incrementAndGet());
assertSame(nativeNear, nativeSelected);
assertEquals(1, irisReferences.get());
assertEquals(1, nativeReferences.get());
}
@Test
public void nativePredictionIsReadOnlyUntilTheWinnerIsCommitted() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
String source = Files.readString(nativegen.resolve("NativeStructureVanillaLocator.java")).replace("\r\n", "\n");
int predictionStart = source.indexOf("private static Candidate predictAt(");
int candidateStart = source.indexOf("public static final class Candidate", predictionStart);
String prediction = source.substring(predictionStart, candidateStart);
String candidate = source.substring(candidateStart);
assertFalse(prediction.contains("addReference("));
assertTrue(candidate.contains("structureManager.addReference(referenceStart)"));
assertTrue(candidate.contains("committed.compareAndSet(false, true)"));
}
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"));
String source = Files.readString(processor).replace("\r\n", "\n");
String foundation = Files.readString(
processor.resolveSibling("NativeStructureFoundationBuilder.java")).replace("\r\n", "\n");
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int stiltDefinition = foundation.indexOf("static void placeStilts(");
int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition);
int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(stiltDefinition >= 0);
assertTrue(occupancyCheck > stiltDefinition);
assertTrue(terrainFloor > stiltDefinition);
assertFalse(source.contains("state.equals("));
assertFalse(foundation.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
assertFalse(foundation.contains("snapshot.states"));
}
@Test
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))
.resolveSibling("NativeStructureVerticalPlacer.java")).replace("\r\n", "\n");
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
assertTrue(placementStart >= 0);
assertTrue(shiftStart > placementStart);
assertTrue(alignmentStart > shiftStart);
assertTrue(moveStart > alignmentStart);
assertTrue(movePieceStart > moveStart);
assertTrue(monumentChildrenStart > movePieceStart);
String placementMethod = source.substring(placementStart, shiftStart);
String shiftMethod = source.substring(shiftStart, alignmentStart);
String moveMethod = source.substring(moveStart, movePieceStart);
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
assertTrue(placementMethod.contains("return applyVerticalShift("));
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
}
}
@@ -1,29 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingPlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public int[] placeStructure(");
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
String method = source.substring(methodStart, methodEnd);
int catchStart = method.indexOf("catch (RuntimeException e)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -1,103 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingServerLevelAdviceContractTest {
@Test
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(transformer.contains("takesArgument(0, MinecraftServer.class)"));
assertTrue(transformer.contains("takesArgument(5, LevelStem.class)"));
assertFalse(transformer.contains("takesArgument(12, ChunkGenerator.class)"));
assertFalse(transformer.contains("takesArgument(13, ChunkGenerator.class)"));
assertTrue(advice.contains("@Advice.AllArguments Object[] constructorArguments"));
assertTrue(advice.contains("argument instanceof ChunkGenerator candidate"));
assertFalse(advice.contains("@Advice.Argument(12)"));
assertFalse(advice.contains("@Advice.Argument(13)"));
}
@Test
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class LevelStorageAccessAdvice",
"private static class ServerLevelAdvice");
assertTrue(transformer.contains(
".type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))"));
assertTrue(transformer.contains("Advice.to(LevelStorageAccessAdvice.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArguments(4)"));
assertTrue(transformer.contains(
"ElementMatchers.takesArgument(0, LevelStorageSource.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(1, String.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(2, Path.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(3, ResourceKey.class)"));
assertTrue(advice.contains("@Advice.Argument(1) String levelId"));
assertTrue(advice.contains(
"@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType"));
assertTrue(advice.contains("getDeclaredMethod(\"peekStemGenerator\", String.class)"));
assertTrue(advice.contains(".invoke(null, levelId)"));
assertTrue(advice.contains(
"dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);"));
assertFalse(advice.contains("dimensionRoot("));
assertFalse(advice.contains("Files.copy"));
}
@Test
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(advice.contains(
"@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey"));
assertTrue(advice.contains("getMethod(\"getTarget\")"));
assertTrue(advice.contains("getMethod(\"getWorld\")"));
assertTrue(advice.contains("getMethod(\"identity\")"));
assertTrue(advice.contains("\"iris\".equals(worldIdentifier.getNamespace())"));
assertTrue(advice.contains("\"minecraft\".equals(worldIdentifier.getNamespace())"));
int resolvedStem = advice.indexOf("Object resolvedStem = stemMethod.invoke(");
int validatedStem = advice.indexOf("resolvedStem instanceof LevelStem runtimeStem");
int identityPublication = advice.indexOf(
"dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);");
int stemPublication = advice.indexOf("levelStem = runtimeStem;");
assertTrue(resolvedStem >= 0);
assertTrue(validatedStem > resolvedStem);
assertTrue(identityPublication > validatedStem);
assertTrue(stemPublication > identityPublication);
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -1,28 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class NMSBindingShutdownBoundaryContractTest {
@Test
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
assertTrue(boundary.contains("server.hasFullyShutdown"));
assertTrue(boundary.contains("server.getRunningThread()"));
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -1,43 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingStructureFailureContractTest {
@Test
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
List<String> methodNames = List.of(
"getStructureKeys",
"getStructureSetKeys",
"getReachableStructureKeys",
"getStructureBiomeKeys",
"getPossibleBiomeKeys");
for (String methodName : methodNames) {
String method = methodSource(source, methodName);
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
assertFalse(methodName + " must not log and return an empty registry result",
method.contains("IrisLogging.reportError"));
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
}
}
private static String methodSource(String source, String methodName) {
int start = source.indexOf("public KList<String> " + methodName + "(");
int end = source.indexOf("\n @Override", start + 1);
if (start < 0 || end < 0) {
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
}
return source.substring(start, end);
}
}
@@ -1,30 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class NMSBindingTileDeserializeContractTest {
@Test
public void ownedRegionTileDataMergesBeforeScheduledFallback() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public void deserializeTile(");
int methodEnd = source.indexOf("\n private void merge(", methodStart);
String method = source.substring(methodStart, methodEnd);
int ownershipCheck = method.indexOf("J.isOwnedByCurrentRegion(");
int synchronousMerge = method.indexOf("merge(level, blockPos, tag);", ownershipCheck);
int synchronousReturn = method.indexOf("return;", synchronousMerge);
int scheduledFallback = method.indexOf("J.runAt(pos, () -> merge(level, blockPos, tag))");
assertTrue(ownershipCheck >= 0);
assertTrue(synchronousMerge > ownershipCheck);
assertTrue(synchronousReturn > synchronousMerge);
assertTrue(scheduledFallback > synchronousReturn);
}
}
@@ -1,38 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* A registered structure with an EMPTY biome filter is legal datapack content: opt-in structures
* (e.g. Towns and Towers' towns_and_towers:exclusives/*) ship biome tags that only reference
* optional modded biomes, so the filter resolves to zero entries on a vanilla server. Resolving
* biome keys for such a structure must return an empty set (the structure is simply unreachable),
* never throw — a throw here aborts the whole /iris structure verify run and degrades /iris find.
* The defensive throw is only kept for a NON-empty filter whose holders all fail to resolve keys.
*/
public class VanillaStructureBiomesEmptyFilterContractTest {
@Test
public void emptyBiomeFilterReturnsEmptyKeysInsteadOfThrowing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.vanillaStructureBiomesSource")));
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
assertFalse(source.contains("exposes no registered biome keys"));
}
@Test
public void moddedHookMatchesEmptyFilterContract() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.moddedStructureHooksSource")));
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
assertFalse(source.contains("exposes no registered biome keys"));
}
}
@@ -0,0 +1,130 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.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 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.anyInt;
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;
public final class IrisChunkGeneratorHeightCacheTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
}
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
}
@Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
}
@Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null));
}
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
}
private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "engine", engine);
set(generator, "terrainHeights", new NativeTerrainHeightCache());
return new Fixture(generator, engine, complex);
}
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.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(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
}
}
@@ -0,0 +1,125 @@
package art.arcane.iris.nativegen;
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(objects, stack, -64);
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
verifyNoInteractions(stack);
assertFalse(NativeGenerationWriteGuard.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(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(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
assertFalse(NativeGenerationWriteGuard.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, region, new ChunkPos(0, 0)));
ChunkAccess complete = mock(ChunkAccess.class);
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
when(region.getChunk(1, -1)).thenReturn(complete);
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
}
@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, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
}
}
@@ -1,7 +1,7 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisJigsawConfiguration;
import art.arcane.iris.engine.object.IrisStructureTerrain;
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;
@@ -59,9 +59,6 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -316,40 +313,6 @@ public class NativeStructureFactoryTest {
assertEquals(1, oversized.getPieces().size());
}
@Test
public void failedManualGenerationClearsStaleOwnershipAfterReplacementHandling() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
StandardCharsets.UTF_8);
int failureStart = source.indexOf("if (!isUsableGeneratedStart(generated))");
int failureEnd = source.indexOf("continue;", failureStart);
String failureBranch = source.substring(failureStart, failureEnd);
assertTrue(failureBranch.contains("if (replacement)"));
assertTrue(failureBranch.indexOf("NativeStructureOwnershipStore.discard(")
> failureBranch.lastIndexOf("}") );
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipFingerprint.capture("));
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
}
@Test
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
StandardCharsets.UTF_8);
int ownershipRecord = source.indexOf("NativeStructureOwnershipStore.record(");
int publication = source.indexOf("context.structureManager().setStartForStructure(", ownershipRecord);
int cleanup = source.indexOf("NativeStructureOwnershipStore.discard(", publication);
int suppressed = source.indexOf("publicationError.addSuppressed(cleanupError)", cleanup);
int rethrow = source.indexOf("throw publicationError;", suppressed);
assertTrue(ownershipRecord >= 0);
assertTrue(publication > ownershipRecord);
assertTrue(cleanup > publication);
assertTrue(suppressed > cleanup);
assertTrue(rethrow > suppressed);
}
private static final class TestChunkGenerator extends ChunkGenerator {
private TestChunkGenerator() {
super(new FixedBiomeSource(Holder.direct((Biome) null)));
@@ -9,9 +9,6 @@ import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
@@ -24,20 +21,6 @@ public class NativeStructureGenerationKeysContractTest {
Bootstrap.bootStrap();
}
@Test
public void reachabilityUsesEnabledActiveStructureSetsWithoutLazyPlacementGeneration() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.nativeStructureGenerationKeysSource")));
assertTrue(source.contains("getWorldGenSettings().options().generateStructures()"));
assertTrue(source.contains("getGeneratorState().possibleStructureSets()"));
assertTrue(source.contains("isEnabledPlacement(structureSet.placement())"));
assertTrue(source.contains("StructurePlacement.class.getDeclaredMethods()"));
assertTrue(source.contains("entry.weight() <= 0"));
assertTrue(source.contains("hasPossibleBiome(entry.structure().value(), possibleBiomes)"));
assertFalse(source.contains("getPlacementsForStructure("));
}
@Test
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
@@ -1,11 +1,11 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -1,12 +1,12 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.StructurePlacementGrid;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
@@ -1,9 +1,9 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.engine.object.IrisStructureYBand;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.structure.placement.IrisStructureYBand;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -39,11 +39,8 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProc
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
@@ -221,18 +218,6 @@ public class NativeStructurePostProcessorEncaseTest {
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
}
@Test
public void encaseFillRunsBeforeNativePlacement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", prepareTerrain);
int placement = source.indexOf("placeVanillaStructure(world, structureManager, random", placementLoop);
assertTrue(prepareTerrain >= 0);
assertTrue(placementLoop > prepareTerrain);
assertTrue(placement > placementLoop);
}
@Test
public void omittedTerrainConfigurationRetainsSourceSemantics() {
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
@@ -1,7 +1,7 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
@@ -1,9 +1,9 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.generation.mantle.StructureCarvingFootprint;
import art.arcane.iris.structure.placement.IrisStructureCarveShape;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
@@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
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.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
@@ -30,7 +31,9 @@ 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.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;
@@ -51,6 +54,9 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.ProcessorRule
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
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.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure;
import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
@@ -87,7 +93,7 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorSurfaceTerrainTest {
private static final int SLAB_DEPTH = 128;
private static final int SLAB_DEPTH = 512;
private static final int SLAB_MIN_Y = 64;
private static final int SLAB_PADDING = 14;
private static final int SLAB_WIDTH = 16;
@@ -99,6 +105,512 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Bootstrap.bootStrap();
}
@Test
public void flattenCutsAndFillsSymmetricallyAcrossItsConfiguredBlend() {
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 48);
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = List.of(anchor);
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 128).targetY());
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 32).targetY());
assertEquals(152, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 200).targetY());
assertEquals(48, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 0).targetY());
int previousCut = 80;
for (int x = 1; x <= 24; x++) {
int cut = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 128).targetY();
int fill = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 32).targetY();
assertEquals(160, cut + fill);
assertTrue(cut >= previousCut);
assertTrue(cut - previousCut <= 3);
previousCut = cut;
}
assertEquals(128, previousCut);
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 24, 0, 128).range());
}
@Test
public void flattenBlendsNeighboringFoundationsWithoutOrderDependentRidges() {
NativeStructureSurfaceFitter.FlattenAnchor left = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 64);
NativeStructureSurfaceFitter.FlattenAnchor right = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(8, 8, 0, 0, 80, 2), 24, 64);
int previous = 80;
for (int x = 0; x <= 8; x++) {
NativeStructureSurfaceFitter.FlattenResolution forward = NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(left, right), x, 0, 104);
assertEquals(forward, NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(right, left), x, 0, 104));
assertTrue(forward.targetY() <= 82);
assertTrue(Math.abs(forward.targetY() - previous) <= 1);
previous = forward.targetY();
}
}
@Test
public void flattenBlendsAnElevatedRoadJunctionWithNearbyFoundations() {
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = new ArrayList<>();
for (int x : List.of(-3, 3)) {
for (int z = -1; z <= 1; z++) {
anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(x, x, z, z, 64, 2), 24, 64));
}
}
anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64));
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
NativeStructureSurfaceFitter.FlattenResolution grade =
NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, z, 96);
assertTrue(grade.targetY() >= 64 && grade.targetY() <= 72);
assertEquals(grade, NativeStructureSurfaceFitter.resolveFlattenSurface(
anchors.reversed(), x, z, 96));
if (x < 1) {
assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface(
anchors, x + 1, z, 96).targetY()) <= 1);
}
if (z < 1) {
assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface(
anchors, x, z + 1, 96).targetY()) <= 1);
}
}
}
}
@Test
public void flattenPreservesAnExactRigidFloorBesideAnElevatedJunction() {
NativeStructureSurfaceFitter.FlattenAnchor rigid = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 24, 64);
NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64);
assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(rigid, junction), 0, 0, 96).targetY());
assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(junction, rigid), 0, 0, 96).targetY());
}
@Test
public void flattenRetainsIsolatedJunctionSupportWithinItsConfiguredBounds() {
for (int radius : List.of(0, 24)) {
NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), radius, 8);
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = List.of(junction);
assertEquals(104, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).targetY());
assertEquals(8, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).range());
int outside = Math.max(1, radius);
assertEquals(96, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).targetY());
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).range());
}
}
@Test
public void flattenLevelsNonJigsawFoundationsAndRemovesDetachedRoofs() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 3, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = 0; x <= 3; x++) {
int sourceY = x < 2 ? groundY - 32 : groundY + 32;
put(blocks, x, 12, 0, Blocks.STONE.defaultBlockState());
put(blocks, x, sourceY - 1, 0, Blocks.DIRT.defaultBlockState());
put(blocks, x, sourceY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
assertTrue(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
(x, z) -> x < 2 ? groundY - 32 : groundY + 32);
for (int x = 0; x <= 3; x++) {
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, x, groundY, 0));
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
}
}
@Test
public void flattenRestoresAnUnchangedFoundationSurfaceOverADeepAirGap() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY - 19, 0, Blocks.DIRT.defaultBlockState());
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 19; y <= groundY; y++) {
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, y, 0));
}
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> buried = flatTerrain(area, 100);
Map<BlockPos, BlockState> originalBuried = new HashMap<>(buried);
Map<BlockPos, BlockState> submerged = flatTerrain(area, groundY - 8);
put(submerged, 0, groundY - 7, 0, Blocks.WATER.defaultBlockState());
Map<BlockPos, BlockState> originalSubmerged = new HashMap<>(submerged);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(buried), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> 100);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(submerged), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8);
assertEquals(originalBuried, buried);
assertEquals(originalSubmerged, submerged);
}
@Test
public void flattenExemptsUndergroundAndSubmergedStructureTypes() {
Structure.StructureSettings surface = new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE);
Structure.StructureSettings underground = new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.UNDERGROUND_STRUCTURES, TerrainAdjustment.NONE);
List<Structure> exempt = List.of(new StrongholdStructure(surface), new OceanMonumentStructure(surface),
new ShipwreckStructure(surface, false), new DesertPyramidStructure(underground));
for (Structure structure : exempt) {
StructureStart start = new StructureStart(structure, new ChunkPos(0, 0), 0,
new PiecesContainer(desertStart().getPieces()));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
}
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.BURY), IrisStructureTerrainMode.FLATTEN)));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), IrisStructureTerrainMode.FLATTEN)));
}
@Test
public void flattenFoundationReadsAndWritesStayInsideConfiguredVerticalRange() throws Exception {
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 128, 0, 0, 136, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 0, 255, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 94, 0, Blocks.GOLD_BLOCK.defaultBlockState());
WorldGenLevel delegate = world(blocks);
WorldGenLevel bounded = (WorldGenLevel) Proxy.newProxyInstance(WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, (proxy, method, arguments) -> {
if (method.getName().equals("getBlockState") || method.getName().equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
assertTrue(position.toString(), position.getY() >= 95 && position.getY() <= 159);
}
return method.invoke(delegate, arguments);
});
NativeStructureSurfaceFitter.prepareSurfaceStructures(bounded, area, List.of(target), (x, z) -> 127);
assertEquals(Blocks.GOLD_BLOCK.defaultBlockState(), state(blocks, 0, 94, 0));
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 95, 0));
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 127, 0));
assertTrue(state(blocks, 0, 128, 0).isAir());
}
@Test
public void flattenReferencesIncludeTheMaximumConfiguredFeather() throws Exception {
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)
.setHorizontalPadding(128).setFlattenRange(128);
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(start, start.getStructure(), terrain);
assertEquals(-128, references.minX());
assertEquals(128, references.maxX());
assertEquals(-128, references.minZ());
assertEquals(128, references.maxZ());
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 128, 128);
assertTrue(NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 127, 0, 100).range() > 0);
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 128, 0, 100).range());
}
@Test
public void flattenPreservesNativeVillageSupportBridges() throws Exception {
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0);
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written);
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0));
}
@Test
public void flattenDoesNotBridgeACompletelyBuriedVillage() throws Exception {
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
BoundingBox area = new BoundingBox(0, 58, 0, 0, 90, 0);
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
put(blocks, 0, 80, 0, Blocks.STONE.defaultBlockState());
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertTrue(written.isEmpty());
assertTrue(state(blocks, 0, 63, 0).isAir());
}
@Test
public void flattenKeepsPyramidPlacementAboveItsPreparedGround() {
FlattenDesertPiece piece = new FlattenDesertPiece();
StructureStart start = new StructureStart(desertStart().getStructure(), new ChunkPos(0, 0), 0,
new PiecesContainer(List.of(piece)));
BoundingBox footprint = piece.getBoundingBox();
NativeStructureVerticalPlacer.applyVerticalPlacement(start, "minecraft:desert_pyramid", 0,
63, 0, 256, false, false, null, (x, z) -> x == footprint.minX() ? 80 : 112);
BoundingBox area = new BoundingBox(footprint.minX(), 0, footprint.minZ(),
footprint.maxX(), 160, footprint.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = footprint.minX(); x <= footprint.maxX(); x++) {
for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) {
int surfaceY = x == footprint.minX() ? 80 : 112;
put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState());
}
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
(x, z) -> x == footprint.minX() ? 80 : 112);
assertEquals(81, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(81, piece.getBoundingBox().minY());
assertTrue(state(blocks, footprint.maxX(), 80, footprint.maxZ()).isSolid());
assertTrue(state(blocks, footprint.maxX(), 81, footprint.maxZ()).isAir());
}
@Test
public void flattenLetsTerrainMatchingPathsFollowThePreparedVillageGrade() throws Exception {
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 4, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 128, 0);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 96);
for (int x : List.of(0, 8)) {
blocks.remove(new BlockPos(x, 95, 0));
blocks.remove(new BlockPos(x, 96, 0));
put(blocks, x, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, x, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
WorldGenLevel world = world(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world, area, List.of(target),
(x, z) -> x == 0 || x == 8 ? 64 : 96);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertTrue(state(blocks, 4, 96, 0).isAir());
assertEquals(1, pathGround.size());
int pathY = pathGround.values().iterator().next().y();
assertTrue("Path Y " + pathY, pathY >= 64 && pathY <= 67);
}
@Test
public void flattenPublishesCutAndFillHeightsBeforeNativePathProjectionAtFeatures() throws Exception {
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(0, 256), containerFactory(), null);
chunk.setPersistedStatus(ChunkStatus.CARVERS);
for (int x = 0; x <= 8; x++) {
int originalY = x < 4 ? 96 : 32;
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
WorldgenTerrainHeightmaps.primeTerrain(chunk,
(x, z) -> x == 15 ? 81 : x < 4 ? 97 : 33,
(x, z) -> x == 15 ? 61 : x < 4 ? 97 : 33);
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(8, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(0, 65, 0, 8, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, List.of(target), (x, z) -> x < 4 ? 96 : 32);
assertEquals(96, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 0, 0));
assertEquals(32, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 0));
plan.primeHeightmaps(chunk);
chunk.setBlockState(new BlockPos(0, 85, 0), Blocks.OAK_PLANKS.defaultBlockState(), 2);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertEquals(2, pathGround.size());
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
assertEquals(64, cell.y());
chunk.setBlockState(new BlockPos(cell.x(), cell.y(), cell.z()), cell.occupancy().state(), 2);
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 63, cell.z())).isSolid());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 65, cell.z())).isAir());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 66, cell.z())).isAir());
assertEquals(64, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, cell.x(), cell.z()));
assertEquals(64, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, cell.x(), cell.z()));
}
assertEquals(80, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 15, 0));
assertEquals(60, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, 15, 0));
}
@Test
public void flattenSupportsProjectedRoadsAndTheirSurroundingGradeAcrossRoofCavities() throws Exception {
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(0, 256), containerFactory(), null);
chunk.setPersistedStatus(ChunkStatus.CARVERS);
for (int x = 0; x <= 8; x++) {
int originalY = x == 0 || x == 8 ? 64 : 96;
write(chunk, x, 20, 0, Blocks.STONE.defaultBlockState());
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
write(chunk, 6, 50, 0, Blocks.WATER.defaultBlockState());
WorldgenTerrainHeightmaps.primeTerrain(chunk,
(x, z) -> x == 0 || x == 8 ? 65 : 97,
(x, z) -> x == 0 || x == 8 ? 65 : 97);
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(1, 0, 0, Blocks.AIR.defaultBlockState()),
block(2, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 6, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24).setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, List.of(target), (x, z) -> x == 0 || x == 8 ? 64 : 96);
plan.primeHeightmaps(chunk);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertEquals(2, pathGround.size());
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
write(chunk, cell.x(), cell.y(), cell.z(), cell.occupancy().state());
assertTrue(cell.y() > 64 && cell.y() < 72);
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() - 1, cell.z())).isSolid());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() + 1, cell.z())).isAir());
}
int roadY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 4, 0);
for (int y = roadY - 32; y < roadY; y++) {
assertTrue("Missing road support at " + y, chunk.getBlockState(new BlockPos(4, y, 0)).isSolid());
}
assertTrue(chunk.getBlockState(new BlockPos(4, roadY - 33, 0)).isAir());
int adjacentY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 5, 0);
for (int y = adjacentY - 32; y < adjacentY; y++) {
assertTrue("Missing ground between road cells at " + y,
chunk.getBlockState(new BlockPos(5, y, 0)).isSolid());
}
assertEquals(Blocks.WATER.defaultBlockState(), chunk.getBlockState(new BlockPos(6, 50, 0)));
assertTrue(chunk.getBlockState(new BlockPos(6, 49, 0)).isAir());
}
@Test
public void flattenBuildsAContinuousVillageGradeBetweenSparseFoundations() throws Exception {
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
List<PoolElementStructurePiece> pieces = new ArrayList<>();
for (int x : List.of(0, 8)) {
for (int z : List.of(0, 8)) {
pieces.add(rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(x, 65, z, x, 75, z), 0, Rotation.NONE));
}
}
StructureStart start = rigidSurfaceStart(pieces);
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24).setFlattenRange(64);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 8);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = 0; x <= 8; x++) {
for (int z = 0; z <= 8; z++) {
int originalY = (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96;
put(blocks, x, 20, z, Blocks.STONE.defaultBlockState());
put(blocks, x, originalY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, originalY, z, Blocks.GRASS_BLOCK.defaultBlockState());
}
}
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area, List.of(target),
(x, z) -> (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96);
for (int x = 0; x <= 8; x++) {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
}
}
@Test
public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() {
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
@@ -474,7 +986,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BoundingBox area = new BoundingBox(0, 54, 0, 3, 74, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
@@ -514,7 +1026,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
@@ -546,13 +1058,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> wideBlocks = flatTerrain(wideArea, 60);
Map<BlockPos, BlockState> splitBlocks = flatTerrain(wideArea, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan widePlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan widePlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(wideBlocks), wideArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan westPlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan westPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), westArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan eastPlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan eastPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), eastArea, List.of(target), (x, z) -> 60);
for (int x = 15; x <= 16; x++) {
@@ -2203,6 +2715,16 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
return template;
}
private static final class FlattenDesertPiece extends DesertPyramidPiece {
private FlattenDesertPiece() {
super(RandomSource.create(29L), 0, 0);
}
private boolean attemptVanillaAlignment() {
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, -2);
}
}
private static final class InlineSinglePoolElement extends SinglePoolElement {
private InlineSinglePoolElement(StructureTemplate template) {
this(template, List.of());
@@ -2319,6 +2841,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getHeight") && arguments != null && arguments.length == 3) {
return chunk.getHeight((Heightmap.Types) arguments[0], (int) arguments[1], (int) arguments[2]) + 1;
}
if (methodName.equals("getLevel")) {
return null;
}
@@ -188,6 +188,69 @@ public class NativeStructurePostProcessorVegetationTest {
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 loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.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)));
}
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
@@ -1,7 +1,9 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
@@ -17,15 +19,35 @@ import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.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";
@@ -75,6 +97,151 @@ public class NativeStructureVolumeIndexTest {
assertEquals(coldResolutions, resolver.resolutions());
}
@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());
NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context(
null, null, null, null, () -> null, () -> null, () -> null);
NativeStructureVolumeIndex.install(engine, context);
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));
@@ -108,24 +275,172 @@ public class NativeStructureVolumeIndexTest {
}
@Test
public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() 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));
assertFalse(source.contains("StructureManager"));
assertFalse(source.contains("getStartForStructure"));
assertFalse(source.contains("getAllStarts"));
assertFalse(source.contains("ChunkAccess"));
assertFalse(source.contains("NativeStructureOwnershipStore"));
assertFalse(source.contains("NativeStructureOwnershipRecovery"));
assertFalse(resolver.awaitSecondEntry(250));
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 vanillaVolumesAreGatedOnPackPolicy() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
assertQueuedTerrainCanComplete(false);
}
assertTrue(source.contains("NativeStructureGenerationPolicy.resolve("));
assertTrue(source.contains("if (!decision.generate())"));
assertTrue(source.contains("isStructureChunk("));
@Test
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() 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.resolve(null, 0, 0, 15, 15));
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);
});
awaitOriginWindowWaiter(firstWaiter);
awaitOriginWindowWaiter(secondWaiter);
if (interruptWaiter) {
firstWaiter.get().interrupt();
}
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
terrainTask.get(3, TimeUnit.SECONDS);
assertEquals(289, 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 awaitOriginWindowWaiter(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(NativeStructureVolumeIndex.class.getName())
&& frame.getMethodName().equals("lockOriginWindow")) {
return;
}
}
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
throw new AssertionError("Native query did not wait for the occupied origin window");
}
@Test
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
int distantChunkX = findDisjointWindow(0, 0);
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) {
@@ -134,6 +449,38 @@ public class NativeStructureVolumeIndexTest {
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 int findDisjointWindow(int chunkX, int chunkZ) {
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
for (int candidate = 64; candidate < 16_384; candidate += 17) {
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
return candidate;
}
}
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
}
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final int originChunkX;
@@ -157,4 +504,58 @@ public class NativeStructureVolumeIndexTest {
return NativeStructureVolumeIndex.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,129 @@
package art.arcane.iris.nativegen;
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,105 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.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";
PlatformBlockState custom = mock(PlatformBlockState.class);
PlatformBlockState carrier = mock(PlatformBlockState.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, height);
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
}
}
@Test
public void preservesNativePropertiesCavesAndFluids() {
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
LevelHeightAccessor.create(-16, 32));
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, Heightmap.Types.WORLD_SURFACE_WG, height));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
}
@Test
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
}
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,64 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
assertNotEquals(
CustomBiomeSource.customBiomeKey(host, "Aurora"),
CustomBiomeSource.customBiomeKey(upper, "Aurora")
);
}
@Test
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
}
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.iris.nativegen.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.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,144 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.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 IrisChunkGenerator generator = mock(IrisChunkGenerator.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);
CustomBiomeSource source = mock(CustomBiomeSource.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, "engine", engine);
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());
}
}
}
@@ -1,9 +1,9 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisStructureSetFrequencyOverride;
import art.arcane.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;
@@ -29,8 +29,6 @@ import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -42,7 +40,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NMSBindingDatapackStructureScopeTest {
private static final String SOURCE = "https://example.test/managed.zip";
@@ -351,103 +348,6 @@ public class NMSBindingDatapackStructureScopeTest {
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
}
@Test
public void standardPublishesOneDeferredStateWhileJigsawPublishesInitializedEmptyState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
int methodEnd = source.indexOf(
"\n @Override\n public CompletableFuture<Void> completeStudioStructureBootstrap",
methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
int jigsawMode = method.indexOf(
"boolean jigsawStudio = platformGenerator != null");
int jigsawOnly = method.indexOf("if (jigsawStudio)");
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
int emptyPublication = method.indexOf("stateField.set(chunkMap, bootstrapState);");
int standardOnly = method.indexOf("else if (studioBootstrap)");
int retention = method.indexOf("irisGenerator.retainStudioStructureState(");
int standardPublication = method.indexOf("stateField.set(chunkMap, scopedState);");
int immediateInitialization = method.indexOf("initializeAndPublishStructureState(");
assertTrue(filteredState >= 0);
assertTrue(jigsawMode > filteredState);
assertTrue(jigsawOnly > jigsawMode);
assertTrue(emptyCreation > jigsawOnly);
assertTrue(emptyFiltering > emptyCreation);
assertTrue(emptyInitialization > emptyFiltering);
assertTrue(emptyPublication > emptyInitialization);
assertTrue(standardOnly > emptyPublication);
assertTrue(retention > standardOnly);
assertTrue(standardPublication > retention);
assertTrue(immediateInitialization > standardPublication);
assertFalse(method.contains("scopedState.ensureStructuresGenerated();"));
assertFalse(method.contains("if (studioBootstrap && platformGenerator.isJigsawStudioActive())"));
}
@Test
public void standardCompletionReturnsTheExactActivationFutureWithoutReplacingState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public CompletableFuture<Void> completeStudioStructureBootstrap(World world)");
int methodEnd = source.indexOf("\n @Override\n public void abandonStudioStructureBootstrap", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int retained = method.indexOf("generator.retainedStudioStructureState(level, chunkMap)");
int emptyCompletion = method.indexOf("return CompletableFuture.completedFuture(null);");
int activation = method.indexOf("return generator.activateStudioStructureState(retained);");
assertTrue(retained >= 0);
assertTrue(emptyCompletion > retained);
assertTrue(activation > retained);
assertFalse(method.contains("stateField.set("));
assertFalse(method.contains("retained.fullState()"));
}
@Test
public void injectionVerifiesTheCanonicalPaperGeneratorBeforeStructureRetargeting() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public void inject(long seed, Engine engine, World world)");
int methodEnd = source.indexOf("\n @Override\n public DatapackStructureScopeResult", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int publication = method.indexOf("worldGenContextField.set(chunkMap, newContext);");
int canonicalRead = method.indexOf("level.getChunkSource().getGenerator()", publication);
int identityGate = method.indexOf("activeGenerator != irisGenerator", canonicalRead);
int structureRetarget = method.indexOf("retargetStructureCheck(level, irisGenerator)", identityGate);
assertTrue(publication >= 0);
assertTrue(canonicalRead > publication);
assertTrue(identityGate > canonicalRead);
assertTrue(structureRetarget > identityGate);
}
@Test
public void structureStateRecreationUsesTheWorldOwnedSpigotConfiguration() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("private ChunkGeneratorStructureState createStructureState(");
int methodEnd = source.indexOf(
"\n private void initializeAndPublishStructureState(", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("level.spigotConfig"));
assertFalse(method.contains("currentState.conf"));
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
@@ -0,0 +1,250 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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());
}
}
}
@@ -1,9 +1,14 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import 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;
@@ -12,10 +17,25 @@ 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;
@@ -30,11 +50,13 @@ 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 {
@@ -49,6 +71,69 @@ public class NativeStructureWorldgenAccessTest {
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(IrisStructureTerrainMode.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();
@@ -135,6 +220,54 @@ public class NativeStructureWorldgenAccessTest {
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();
@@ -183,7 +316,9 @@ public class NativeStructureWorldgenAccessTest {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y);
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
}
private static ChunkPos generationCenter() {
@@ -203,6 +338,7 @@ public class NativeStructureWorldgenAccessTest {
private int statusChunkReads;
private int mutations;
private int events;
private boolean rejectDistantTerrainReads;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
@@ -243,6 +379,11 @@ public class NativeStructureWorldgenAccessTest {
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;
}
@@ -0,0 +1,100 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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 NMSBinding binding = mock(NMSBinding.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,95 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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);
NMSBinding binding = mock(NMSBinding.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);
NMSBinding binding = mock(NMSBinding.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);
NMSBinding binding = mock(NMSBinding.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,103 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
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);
NMSBinding binding = mock(NMSBinding.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);
NMSBinding binding = mock(NMSBinding.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);
NMSBinding binding = mock(NMSBinding.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);
NMSBinding binding = mock(NMSBinding.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,114 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.history.SavedTerrainChunk;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
public class NmsSavedTerrainCheckpointTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{1, 2, 3};
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
Set<ChunkPos> completed = new HashSet<>();
AtomicBoolean flushed = new AtomicBoolean();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(saved.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(unloaded.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
flushed.set(true);
return null;
});
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
.thenAnswer(invocation -> {
assertTrue(flushed.get());
return null;
});
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
terrain.verifyNoMoreInteractions();
}
}
@Test
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
IOException failure = new IOException("storage flush failed");
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
terrain.verifyNoInteractions();
}
}
@Test
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{4, 5};
IOException failure = new IOException("saved terrain receipt mismatch");
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenReturn(Priority.COMPLETING);
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
}
}
}
@@ -0,0 +1,223 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainSnapshotTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
Thread owner = Thread.currentThread();
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
PlatformHooks platform = mock(PlatformHooks.class);
CompoundTag capturedData = new CompoundTag();
CompoundTag autosave = new CompoundTag();
List<String> submissions = new ArrayList<>();
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return snapshot;
});
hooks.when(PlatformHooks::get).thenReturn(platform);
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return null;
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
when(snapshot.write()).thenAnswer(invocation -> {
assertNotSame(owner, Thread.currentThread());
assertEquals(List.of("checkpoint", "autosave"), submissions);
return capturedData;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
.thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
submissions.add("checkpoint");
return null;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
submissions.add("autosave");
return null;
});
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
assertEquals(List.of("checkpoint"), submissions);
assertFalse(captured.data().isDone());
verify(snapshot, never()).write();
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
verify(snapshot).write();
}
}
@Test
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
IllegalStateException failure = new IllegalStateException("serialization failed");
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
when(snapshot.write()).thenThrow(failure);
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
assertTrue(captured.data().isCompletedExceptionally());
}
}
@Test
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
World world = mock(World.class);
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
List<NewChunkHolder> holders = new ArrayList<>(129);
for (int index = 0; index < 129; index++) {
NewChunkHolder holder = mock(NewChunkHolder.class);
setField(holder, "chunkX", index);
holders.add(holder);
}
AtomicInteger requested = new AtomicInteger();
AtomicBoolean writesBlocked = new AtomicBoolean(true);
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
Future<?> result = checkpoint.submit(() -> {
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
.thenAnswer(invocation -> {
requested.incrementAndGet();
Runnable action = invocation.getArgument(3);
return CompletableFuture.runAsync(action, owner);
});
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
firstBatchWaiting.countDown();
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
});
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
return null;
}
});
try {
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
assertEquals(64, requested.get());
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
writesBlocked.set(false);
result.get(5, TimeUnit.SECONDS);
assertEquals(129, requested.get());
} finally {
writesBlocked.set(false);
}
}
}
private static void setField(Object instance, String name, Object value) throws Exception {
Field field = instance.getClass().getField(name);
field.setAccessible(true);
field.set(instance, value);
}
private static final class SaveFixture implements AutoCloseable {
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
private final ServerLevel level = mock(ServerLevel.class);
private SaveFixture() throws Exception {
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
setField(scheduler, "saveExecutor", group.createExecutor());
setField(scheduler, "compressionExecutor", group.createExecutor());
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
}
@Override
public void close() throws Exception {
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
}
@@ -0,0 +1,135 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainWriteBarrierTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
ServerLevel level = level(saves, compression);
ChunkPos chunk = new ChunkPos(4, -9);
CountDownLatch generationEntered = new CountDownLatch(1);
CountDownLatch releaseGeneration = new CountDownLatch(1);
AtomicBoolean pendingWrite = new AtomicBoolean(true);
AtomicInteger serialized = new AtomicInteger();
AtomicInteger compressed = new AtomicInteger();
AtomicReference<Thread> saveThread = new AtomicReference<>();
AtomicReference<Thread> compressionThread = new AtomicReference<>();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
generation.queueTask(() -> {
generationEntered.countDown();
try {
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new AssertionError(interruption);
}
});
pool.adjustThreadCount(1);
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
saves.queueTask(() -> {
saveThread.set(Thread.currentThread());
serialized.incrementAndGet();
compression.queueTask(() -> {
compressionThread.set(Thread.currentThread());
compressed.incrementAndGet();
pendingWrite.set(false);
});
});
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
assertEquals(1L, releaseGeneration.getCount());
assertEquals(1, serialized.get());
assertEquals(1, compressed.get());
assertFalse(pendingWrite.get());
assertSame(Thread.currentThread(), saveThread.get());
assertSame(Thread.currentThread(), compressionThread.get());
} finally {
releaseGeneration.countDown();
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
@Test
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
assertTrue(failure.getMessage().contains("Timed out"));
}
@Test
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
Thread.currentThread().interrupt();
try {
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertTrue(failure.getMessage().contains("Interrupted"));
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
savesField.setAccessible(true);
compressionField.setAccessible(true);
savesField.set(scheduler, saves);
compressionField.set(scheduler, compression);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
return level;
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
@@ -64,6 +64,21 @@ public class WorldgenTerrainHeightmapsTest {
Bootstrap.bootStrap();
}
@Test
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
IrisChunkGenerator.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));
@@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
package art.arcane.iris.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureLoss;
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;
+2 -16
View File
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
.get()
dependencies {
@@ -21,8 +21,8 @@ dependencies {
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api)
testImplementation(libs.bstats)
testImplementation(libs.sentry)
testImplementation(project(':core'))
testImplementation(testFixtures(project(':core')))
testImplementation(volmLibCoordinate) {
transitive = false
}
@@ -60,18 +60,4 @@ tasks.named('jar', Jar).configure {
}
tasks.named('test').configure {
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
systemProperty('iris.commandIrisSource', file('src/main/java/art/arcane/iris/core/commands/CommandIris.java').absolutePath)
systemProperty('iris.bukkitChunkGeneratorSource', rootProject.file('core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
systemProperty('iris.jigsawStudioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java').absolutePath)
systemProperty('iris.studioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/StudioSVC.java').absolutePath)
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
systemProperty('iris.startupSource', file('src/main/java/art/arcane/iris/Iris.java').absolutePath)
systemProperty('iris.readmeSource', rootProject.file('README.md').absolutePath)
}
@@ -18,72 +18,78 @@
package art.arcane.iris;
import art.arcane.iris.engine.IrisEngineEffects;
import art.arcane.iris.engine.IrisWorldManager;
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
import art.arcane.iris.generation.runtime.IrisEngineEffects;
import art.arcane.iris.generation.runtime.IrisWorldManager;
import art.arcane.iris.engine.framework.EngineComponentCleanup;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.IrisStartupAdmissionListener;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.core.IrisWorldGeneratorResolver;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.PendingWorldDeleteQueue;
import art.arcane.iris.core.PendingWorldReplacementManager;
import art.arcane.iris.core.SettingsHotloadWatch;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackIngestService.StartupValidationOutcome;
import art.arcane.iris.core.lifecycle.ManagedWorldLoader;
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.WorldDeletionQueue;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.generation.runtime.EngineComponentCleanup;
import art.arcane.iris.generation.runtime.EngineEffectsProvider;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.IrisStartupValidation;
import art.arcane.iris.world.IrisStartupAdmissionListener;
import art.arcane.iris.world.BukkitWorldReconciler;
import art.arcane.iris.world.IrisWorldGeneratorResolver;
import art.arcane.iris.world.IrisWorldStorage;
import art.arcane.iris.world.PendingWorldDeleteQueue;
import art.arcane.iris.world.PendingWorldReplacementManager;
import art.arcane.iris.configuration.SettingsHotloadWatch;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
import art.arcane.iris.world.lifecycle.ManagedWorldLoader;
import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.world.lifecycle.PaperLibBootstrap;
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.world.runtime.WorldDeletionQueue;
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.core.link.IrisPapiInstaller;
import art.arcane.iris.core.link.IrisPapiListener;
import art.arcane.iris.core.link.IrisPapiState;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.CommandSVC;
import art.arcane.iris.core.service.DatapackStructureScopeSVC;
import art.arcane.iris.core.service.EditSVC;
import art.arcane.iris.core.service.EntityRiseSVC;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.IrisIntegrationService;
import art.arcane.iris.core.service.IrisProtocolService;
import art.arcane.iris.core.service.IrisTerrainSVC;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.core.service.LogFilterSVC;
import art.arcane.iris.core.service.MultiverseSVC;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.core.service.PreservationSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.TreeFellerSVC;
import art.arcane.iris.core.service.TreeSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.engine.framework.BlockEditAccess;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IrisCompat;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.integration.IrisPapiInstaller;
import art.arcane.iris.integration.IrisPapiListener;
import art.arcane.iris.integration.IrisPapiState;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
import art.arcane.iris.integration.MultiverseCoreLink;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
import art.arcane.iris.studio.view.BukkitGuiHost;
import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.platform.bukkit.BoardSVC;
import art.arcane.iris.command.CommandSVC;
import art.arcane.iris.pack.datapack.DatapackStructureScopeSVC;
import art.arcane.iris.studio.edit.EditSVC;
import art.arcane.iris.world.entity.EntityRiseSVC;
import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.generation.runtime.GlobalCacheSVC;
import art.arcane.iris.platform.bukkit.api.IrisApiEventSVC;
import art.arcane.iris.generation.runtime.IrisEngineSVC;
import art.arcane.iris.integration.IrisIntegrationService;
import art.arcane.iris.platform.protocol.IrisProtocolService;
import art.arcane.iris.platform.bukkit.api.IrisTerrainSVC;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.diagnostics.LogFilterSVC;
import art.arcane.iris.integration.MultiverseSVC;
import art.arcane.iris.studio.object.ObjectSVC;
import art.arcane.iris.studio.object.ObjectStudioSaveService;
import art.arcane.iris.generation.runtime.PreservationSVC;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.world.tree.TreeFellerSVC;
import art.arcane.iris.world.tree.TreeSVC;
import art.arcane.iris.studio.wand.WandSVC;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.EnginePanic;
import art.arcane.iris.generation.runtime.BlockEditAccess;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.pack.validation.IrisCompat;
import art.arcane.iris.world.safeguard.IrisSafeguard;
import art.arcane.iris.world.safeguard.RuntimeLockNotice;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
@@ -94,7 +100,7 @@ import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.function.NastyRunnable;
import art.arcane.volmlib.util.hud.HudActionBar;
import art.arcane.volmlib.util.hud.HudBossBarLane;
@@ -105,16 +111,16 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.plugin.ComponentLog;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
import art.arcane.volmlib.util.plugin.ComponentText;
import art.arcane.iris.util.common.misc.Bindings;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.misc.SlimJar;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.plugin.chunk.ChunkTickets;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.iris.platform.bootstrap.Bindings;
import art.arcane.iris.platform.bootstrap.ServerProperties;
import art.arcane.iris.platform.bootstrap.SlimJar;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.platform.bukkit.chunk.ChunkTickets;
import art.arcane.iris.world.task.J;
import art.arcane.iris.generation.simd.SimdSupport;
import art.arcane.volmlib.util.scheduling.Queue;
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
import org.bukkit.Bukkit;
@@ -136,11 +142,14 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileDescriptor;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -153,10 +162,12 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@SuppressWarnings("CanBeFinal")
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
@@ -187,6 +198,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
private volatile boolean generatorDrainCompleted;
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
@@ -199,20 +212,30 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
private BukkitLanguageSwitcher languageSwitcher;
private BukkitDebugDump debugDump;
private volatile SettingsHotloadWatch settingsHotloadWatch;
private volatile Thread serverLifecycleThread;
private volatile ServerShutdownBoundary serverShutdownBoundary;
public static VolmitSender getSender() {
if (sender == null) {
sender = new VolmitSender(Bukkit.getConsoleSender());
sender.setTag(instance.getTag());
VolmitSender current = sender;
if (current == null) {
Iris plugin = instance;
current = new VolmitSender(Bukkit.getConsoleSender());
current.setTag(plugin == null ? IrisSafeguard.mode().tag("") : plugin.getTag());
sender = current;
}
return sender;
return current;
}
@SuppressWarnings("unchecked")
public static <T> T service(Class<T> c) {
return (T) instance.services.get(c);
Iris plugin = instance;
if (plugin == null || plugin.services == null) {
throw new IllegalStateException("Iris is disabled; " + c.getSimpleName() + " is unavailable");
}
return (T) plugin.services.get(c);
}
public static void callEvent(Event e) {
@@ -251,7 +274,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public static File getTemp() {
return instance.getDataFolder("cache", "temp");
Iris plugin = instance;
if (plugin == null) {
throw new IllegalStateException("Iris is disabled; the temp folder is unavailable");
}
return plugin.getDataFolder("cache", "temp");
}
public static void msg(String string) {
@@ -293,10 +320,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
StackWalker.StackFrame frame = null;
StackWalker.StackFrame frame;
try {
frame = DEBUG_STACK_WALKER.walk(stream -> stream.skip(1).findFirst().orElse(null));
} catch (Throwable ignored) {
} catch (Throwable unavailable) {
frame = null;
}
if (frame == null) {
@@ -422,14 +450,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (e == null) {
return;
}
if (instance != null && instance.serverStopTeardownDeferred.get()) {
e.printStackTrace(SHUTDOWN_ERRORS);
return;
}
Bindings.capture(e);
boolean debug = false;
if (instance != null) {
try {
IrisSettings currentSettings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get();
debug = currentSettings != null && currentSettings.getGeneral().isDebug();
} catch (Throwable ignored) {
} catch (Throwable unreadable) {
debug = false;
}
}
@@ -455,11 +486,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
Iris.debug("Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage());
}
e.printStackTrace(System.err);
}
public static void reportError(String context, Throwable e) {
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
if (instance != null && instance.serverStopTeardownDeferred.get()) {
SHUTDOWN_ERRORS.println("[Iris] " + message);
error.printStackTrace(SHUTDOWN_ERRORS);
return;
}
try {
if (instance != null) {
@@ -473,7 +511,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
reportError(error);
error.printStackTrace(System.err);
}
public static void dump() {
@@ -520,11 +557,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
Iris::msg,
Iris::reportError,
(event) -> Iris.callEvent((org.bukkit.event.Event) event),
() -> Iris.instance.getDataFolder(),
(path) -> Iris.instance.getDataFile(path),
() -> Iris.instance.getJarFile(),
() -> Iris.instance.getIrisVersion(),
() -> Iris.instance.getMCVersion()));
this::getDataFolder,
this::getDataFile,
this::getJarFile,
this::getIrisVersion,
this::getMCVersion));
SlimJar.load();
}
@@ -591,6 +628,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
return false;
}
EnableTimings timings = new EnableTimings();
alreadyDrained.set(false);
postStopFinisherStarted.set(false);
serverStopTeardownDeferred.set(false);
@@ -598,15 +636,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
sharedRuntimeClosed.set(false);
startupBoundaryRestart.set(false);
terminalCleanupCompleted.set(false);
runtimeTeardownFailed.set(false);
generatorDrainCompleted = false;
deferredShutdownGenerators.clear();
MultiBurst.burst.reopen();
MultiBurst.hydrology.reopen();
MultiBurst.ioBurst.reopen();
IrisLanguage.initialize();
debugDump = BukkitDebugDump.create(this, new BukkitDebugDump.Options(
() -> true,
() -> IrisSafeguard::debugReport));
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
new BukkitLanguageSwitcher.Options("iris", "iris.all",
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
PaperLibBootstrap.install();
SimdSupport.install();
timings.mark("bootstrap");
services = new KMap<>();
BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
Bindings.setupSentry();
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
// enable/disable and paid a full-jar class sweep at boot. Infrastructure first,
// engine/world services next, commands last.
@@ -643,12 +690,13 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
compat = IrisCompat.configured(getDataFile("compat.json"));
IrisServices.register(IrisCompat.class, compat);
timings.mark("services");
ServerConfigurator.configure();
timings.mark("serverConfig");
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
if (datapackValidation == StartupValidationOutcome.READY) {
generatorResolver.validateAllPacks();
}
timings.mark("datapacks");
IrisSafeguard.execute();
timings.mark("safeguard");
getSender().setTag(getTag());
// A cosmetic banner must never abort the bootstrap.
J.attempt(this::splash);
@@ -657,6 +705,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
// Paper's bootstrap runs before any plugin logger exists, so orphan-storage warnings raised there
// never reach logs/latest.log. Replay them once now that the platform log is up.
MissingWorldStorageLog.replayToPlatformLog();
timings.mark("splash");
tickets = new ChunkTickets();
linkMultiverseCore = new MultiverseCoreLink();
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
@@ -669,18 +718,21 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("iris.json"));
settingsHotloadWatch = watch;
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
// packs through cache/temp on an async thread, and a concurrent delete of that folder
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
IO.delete(getTemp());
StudioSVC.gateDownloadsOnStaleTempCleanup(MultiBurst.ioBurst.completeValueAsync(() -> {
IO.delete(getTemp());
return null;
}));
timings.mark("tempSweep");
// One throwing service must not abort the bootstrap: the steps after this loop
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
// Only services that actually enabled get listeners and a later onDisable.
enabledServices.clear();
for (IrisService service : orderedServices) {
long serviceStartedAt = System.nanoTime();
try {
service.onEnable();
enabledServices.add(service);
timings.markService(service.getClass().getSimpleName(), serviceStartedAt);
} catch (Throwable e) {
// A service failure is NOT a datapack validation failure: the admission gate
// must never lock every login over a broken cosmetic service. Log loudly,
@@ -701,12 +753,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
}
}
timings.mark("serviceEnable");
if (datapackValidation == StartupValidationOutcome.READY) {
IrisServices.get(ExternalDataSVC.class).setContentChangeListener(generatorResolver::requestExternalContentRefresh);
generatorResolver.validateAllPacks();
}
timings.mark("packValidation");
addShutdownHook();
pendingWorldReplacements.processPendingStartupReplacements();
pendingWorldDeletes.processPendingStartupWorldDeletes();
if (J.isFolia() && IrisStartupValidation.isReady()) {
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
J.s(this::reconcileStartupWorlds, 1);
}
J.s(() -> {
@@ -721,19 +779,67 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
autoStartStudio();
}
if (!J.isFolia() && IrisStartupValidation.isReady()) {
worldReconciler.checkForBukkitWorlds(s -> true);
reconcileStartupWorlds();
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
// The mantle stores block values as PlatformBlockState, so a BlockData retention can never
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
});
timings.mark("startupTasks");
timings.report();
return true;
}
private static final class EnableTimings {
private static final int REPORTED_SERVICES = 3;
private final long startedAt = System.nanoTime();
private final StringBuilder phases = new StringBuilder();
private final Map<String, Long> serviceMillis = new LinkedHashMap<>();
private long lastMark = startedAt;
private void mark(String phase) {
long now = System.nanoTime();
if (!phases.isEmpty()) {
phases.append(' ');
}
phases.append(phase).append('=').append(TimeUnit.NANOSECONDS.toMillis(now - lastMark)).append("ms");
lastMark = now;
}
private void markService(String service, long serviceStartedAt) {
serviceMillis.put(service, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - serviceStartedAt));
}
private void report() {
IrisLogging.notice("Enabled in %dms (%s)",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt), phases);
String slowest = serviceMillis.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(REPORTED_SERVICES)
.map(entry -> entry.getKey() + "=" + entry.getValue() + "ms")
.collect(Collectors.joining(" "));
if (!slowest.isBlank()) {
IrisLogging.notice("Slowest services: %s", slowest);
}
}
}
private void reconcileStartupWorlds() {
generatorResolver.startupWorldsReady().whenComplete((ignored, failure) -> {
if (failure != null) {
Iris.reportError("Could not resume Iris startup world reconciliation.", failure);
return;
}
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true));
});
}
public void addShutdownHook() {
removeShutdownHook();
serverLifecycleThread = Thread.currentThread();
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -828,6 +934,25 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
.orElse("Iris startup validation requires a restart.");
startupBoundaryRestart.set(true);
ServerConfigurator.restartAtStartupBoundary(restartReason);
return;
}
reportLockedRuntime();
}
private static void reportLockedRuntime() {
String denial = IrisStartupValidation.denialReason().orElse(null);
if (denial == null) {
return;
}
boolean managedStorage;
try {
managedStorage = IrisWorldStorage.hasManagedWorldStorage(IrisWorldStorage.levelRoot());
} catch (Throwable unavailable) {
Iris.reportError("Could not inspect Iris world storage while reporting the locked runtime.", unavailable);
managedStorage = false;
}
for (String line : RuntimeLockNotice.compose(denial, managedStorage)) {
Iris.error(line);
}
}
@@ -862,7 +987,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public BukkitDebugDump debugDump() {
return debugDump;
}
public void selectLanguage(CommandSender sender, String[] arguments) {
BukkitLanguageSwitcher current = languageSwitcher;
if (current != null) {
current.command(sender, arguments);
}
}
public List<String> completeLanguage(CommandSender sender, String[] arguments) {
BukkitLanguageSwitcher current = languageSwitcher;
return current == null ? List.of() : current.complete(sender, arguments);
}
public void onDisable() {
if (debugDump != null) {
debugDump.close();
debugDump = null;
}
if (languageSwitcher != null) {
languageSwitcher.close();
languageSwitcher = null;
}
IrisLanguage.shutdown();
teardownPapi();
boolean serverStopping = IrisToolbelt.isServerStopping();
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
@@ -930,6 +1080,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
drainWorldGenerators(reason, timeoutSeconds);
}
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
return;
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
// Only services whose onEnable actually completed; disabling a service that
// never initialized runs teardown against uninitialized state.
@@ -937,6 +1091,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
service.onDisable();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
}
}
@@ -946,15 +1101,41 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
try {
MultiBurst.burst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris generation workers.", failure);
}
try {
MultiBurst.hydrology.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris hydrology workers.", failure);
}
try {
MultiBurst.ioBurst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris I/O workers.", failure);
}
clearQueues();
IrisServices.clear();
IrisRuntimeStatics.reset();
releaseStatics();
}
}
private static void releaseStatics() {
linkMultiverseCore = null;
compat = null;
tickets = null;
sender = null;
instance = null;
}
private void quiesceRuntimeForServerShutdown(String reason) {
serverStopTeardownDeferred.set(true);
INMS.get().deferPluginClassLoaderClose();
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
if (jigsawStudioService != null) {
try {
@@ -969,7 +1150,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
try {
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS);
if (J.isFolia()) {
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS);
} else {
List<World> worlds = Bukkit.getWorlds();
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS, () -> {
for (World world : worlds) {
INMS.get().pollChunkTask(world);
}
});
}
} catch (Throwable e) {
Iris.reportError("Failed to quiesce the Iris pregenerator before server shutdown.", e);
}
@@ -1027,29 +1217,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
if (!awaitServerShutdownBoundary()) {
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
return;
}
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
}
private boolean awaitServerShutdownBoundary() {
if (!INMS.isBound()) {
ServerShutdownBoundary boundary = serverShutdownBoundary;
if (boundary == null) {
return true;
}
try {
return INMS.get().awaitServerShutdownBoundary(
return boundary.await(
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
} catch (Throwable e) {
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
e.printStackTrace(SHUTDOWN_ERRORS);
return false;
}
}
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
teardownRuntime(reason, timeoutSeconds);
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
return;
}
finishTerminalCleanup();
}
@@ -1061,12 +1257,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
runPostShutdown();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
} finally {
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
runtimeTeardownFailed.set(true);
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
}
IrisPlatforms.unbind();
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
} else {
try {
INMS.get().releasePluginClassLoaderClose();
} catch (Throwable failure) {
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
failure.printStackTrace(SHUTDOWN_ERRORS);
}
}
BukkitPlatform.releaseHost();
}
}
private boolean hasActiveShutdownResources() {
if (!MultiBurst.burst.isTerminated() || !MultiBurst.hydrology.isTerminated()
|| !MultiBurst.ioBurst.isTerminated()) {
return true;
}
return services != null
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
&& preservation.hasActiveResources();
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>();
List<PlatformChunkGenerator> generators = new ArrayList<>();
@@ -1082,6 +1304,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
if (generators.isEmpty()) {
generatorDrainCompleted = true;
Iris.debug("No Iris worlds to freeze.");
return;
}
@@ -1106,12 +1329,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS);
Iris.debug("All Iris chunk generators parked. Safe to unload.");
generatorDrainCompleted = closes.size() == generators.size();
if (generatorDrainCompleted) {
Iris.debug("All Iris chunk generators parked. Safe to unload.");
}
} catch (TimeoutException e) {
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.warn("Iris generator drain interrupted; unload proceeding.");
Iris.reportError("Iris generator drain was interrupted.", e);
} catch (ExecutionException e) {
Iris.reportError(e.getCause() == null ? e : e.getCause());
}
@@ -1245,7 +1471,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public int getIrisVersion() {
String input = Iris.instance.getDescription().getVersion();
String input = getDescription().getVersion();
int hyphenIndex = input.indexOf('-');
if (hyphenIndex != -1) {
String result = input.substring(0, hyphenIndex);
@@ -1,15 +1,15 @@
package art.arcane.iris;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
import art.arcane.iris.core.lifecycle.HuskWorldQuarantine;
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import art.arcane.iris.util.common.misc.SlimJar;
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
import art.arcane.iris.world.lifecycle.HuskWorldQuarantine;
import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import art.arcane.iris.platform.bootstrap.SlimJar;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -1,6 +1,6 @@
package art.arcane.iris;
import art.arcane.iris.util.common.misc.SlimJar;
import art.arcane.iris.platform.bootstrap.SlimJar;
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
@@ -0,0 +1,24 @@
package art.arcane.iris.api.terrain;
import java.util.List;
public record IrisBiomeInfo(
String key,
String name,
String regionKey,
String regionName,
String derivativeKey,
String vanillaDerivativeKey,
String type,
List<IrisCustomBiomeInfo> customDerivatives) {
public IrisBiomeInfo {
key = key == null ? "" : key;
name = name == null ? "" : name;
regionKey = regionKey == null ? "" : regionKey;
regionName = regionName == null ? "" : regionName;
derivativeKey = derivativeKey == null ? "" : derivativeKey;
vanillaDerivativeKey = vanillaDerivativeKey == null ? "" : vanillaDerivativeKey;
type = type == null ? "" : type;
customDerivatives = List.copyOf(customDerivatives);
}
}
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
NATURAL_HEIGHT,
SURFACE_KIND,
BIOME_KEY
BIOME_KEY,
RIVER_STATE,
RIVER_DISTANCE,
RIVER_FLOW,
RIVER_WATER_SURFACE_Y
}
@@ -0,0 +1,64 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisColumnSample(
int blockX,
int blockZ,
int surfaceHeight,
int naturalHeight,
IrisSurfaceKind surfaceKind,
String biomeKey,
IrisRiverState riverState,
double riverDistance,
int riverFlow,
int riverWaterSurfaceY
) {
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
public static final int UNAVAILABLE_RIVER_FLOW = -1;
public IrisColumnSample {
Objects.requireNonNull(surfaceKind, "surfaceKind");
Objects.requireNonNull(riverState, "riverState");
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
}
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
}
}
public boolean hasSurfaceHeight() {
return surfaceHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasNaturalHeight() {
return naturalHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasSurfaceKind() {
return surfaceKind != IrisSurfaceKind.UNKNOWN;
}
public boolean hasBiomeKey() {
return biomeKey != null;
}
public boolean hasRiverState() {
return riverState != IrisRiverState.NONE;
}
public boolean hasRiverDistance() {
return !Double.isNaN(riverDistance);
}
public boolean hasRiverFlow() {
return riverFlow != UNAVAILABLE_RIVER_FLOW;
}
public boolean hasRiverWaterSurfaceY() {
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
}
}
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
void accept(IrisColumnSample sample);
}
@@ -0,0 +1,8 @@
package art.arcane.iris.api.terrain;
public record IrisCustomBiomeInfo(String id, String registryKey) {
public IrisCustomBiomeInfo {
id = id == null ? "" : id;
registryKey = registryKey == null ? "" : registryKey;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.terrain;
public enum IrisRiverState {
NONE,
WET,
DRY
}
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
RIVER,
RIVER_SHORE,
DRY_CHANNEL,
OCEAN,
VOID
}
@@ -18,6 +18,8 @@ public interface IrisTerrainService {
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
@@ -16,23 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
public class CommandDatapack implements DirectorExecutor {
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
public void ingest(
@@ -0,0 +1,39 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.command.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
import org.bukkit.command.CommandSender;
@Director(name = "debug", description = "Iris diagnostic tools", descriptionKey = "iris.director.commanddebug.description")
public final class CommandDebug implements DirectorExecutor {
@Director(name = "dump", sync = true, description = "Create and optionally upload a diagnostic report", descriptionKey = "iris.director.commandiris.director.debugdump")
public void dump(
@Param(name = "upload", defaultValue = "true", description = "Upload the report to mclo.gs", descriptionKey = "iris.director.commandiris.param.debugdump_upload") boolean upload,
@Param(name = "sender", contextual = true) CommandSender sender
) {
Iris.instance.debugDump().request(sender, upload);
}
@Director(sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
ComponentMessenger.sendMarkup(sender, DirectorMiniMenu.version(
"Iris", Iris.instance.getDescription().getVersion(), DirectorMiniMenu.Theme.irisGreen()));
}
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
public void toggle() {
boolean enabled = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(enabled);
IrisSettings.get().forceSave();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG,
MessageArgument.untrusted("to", enabled)));
}
}
@@ -16,40 +16,38 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import com.google.gson.JsonObject;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.runtime.ChunkClearer;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisPackBenchmarking;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.matter.IrisMatterContext;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import art.arcane.iris.world.runtime.ChunkClearer;
import art.arcane.iris.generation.validation.GoldenHashScanner;
import art.arcane.iris.world.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.generation.runtime.IrisEngineSVC;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.pack.IrisPackBenchmarking;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.world.storage.matter.IrisMatterContext;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.world.storage.region.MCAFile;
import art.arcane.iris.world.storage.region.MCAUtil;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
@@ -66,11 +64,11 @@ import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.BukkitRuntimeMessages;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", descriptionKey = "iris.director.commanddeveloper.director.iris_world_manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
@Director(description = "Get Loaded TectonicPlates Count", descriptionKey = "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", origin = DirectorOrigin.BOTH, sync = true)
@@ -79,19 +77,6 @@ public class CommandDeveloper implements DirectorExecutor {
.engineStatus(sender());
}
@Director(description = "Send a test exception to sentry", descriptionKey = "iris.director.commanddeveloper.director.send_test_exception_sentry")
public void Sentry() {
Engine engine = engine();
Exception testException = new Exception("This is a test");
if (engine == null) {
Iris.reportError(testException);
return;
}
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
Iris.reportError(testException);
}
}
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", descriptionKey = "iris.director.commanddeveloper.director.hash_generated_block_output_fixed_area_determinism_identity_testing", origin = DirectorOrigin.BOTH)
public void genhash(
@Param(description = "The world to hash", descriptionKey = "iris.director.commanddeveloper.param.world_hash", contextual = true, contextualOverride = true)
@@ -190,7 +175,7 @@ public class CommandDeveloper implements DirectorExecutor {
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world,
@@ -216,7 +201,7 @@ public class CommandDeveloper implements DirectorExecutor {
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
}
@@ -16,25 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something", descriptionKey = "iris.director.commandedit.director.edit_something")
public class CommandEdit implements DirectorExecutor {
@@ -16,32 +16,38 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.studio.object.ObjectStudioSaveService;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.command.specialhandlers.HydrologyTypeHandler;
import art.arcane.iris.world.history.GenerationFindCatalog;
import art.arcane.iris.command.specialhandlers.LocatableObjectHandler;
import art.arcane.iris.command.specialhandlers.ReachableBiomeHandler;
import art.arcane.iris.command.specialhandlers.ReachableRegionHandler;
import art.arcane.iris.command.specialhandlers.StructureHandler;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
@@ -68,7 +74,7 @@ import java.util.Set;
public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
public void biome(
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look")
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
IrisBiome biome,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
boolean teleport
@@ -85,7 +91,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void region(
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
IrisRegion region,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
boolean teleport
@@ -116,6 +122,75 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find an accepted hydrology feature", sync = true)
public void river(
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
String type,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine activeEngine = engine();
VolmitSender commandSender = sender();
Player target = player();
if (activeEngine == null || commandSender == null || target == null) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
}
return;
}
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
HydrologyFeatureQuery query;
try {
query = HydrologyFeatureQuery.parse(type);
} catch (IllegalArgumentException error) {
commandSender.sendMessage(C.RED + error.getMessage());
return;
}
Location origin = target.getLocation();
World originWorld = origin.getWorld();
int requestedDistance = runtime == null
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
J.a(() -> {
try {
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
activeEngine,
query,
origin.getBlockX(),
origin.getBlockZ(),
maximumDistance,
(int visited) -> sendStructureMessage(target, commandSender,
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
).orElse(null);
if (feature == null) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No accepted " + type + " hydrology feature found within "
+ maximumDistance + " blocks.");
return;
}
String label = type + " hydrology feature";
if (!teleport) {
sendStructureMessage(target, commandSender,
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
return;
}
Location destination = new Location(
originWorld, feature.x(), feature.y(), feature.z());
prepareStructureTeleport(
target, originWorld, commandSender, label, destination, true);
} catch (Throwable error) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
}
});
}
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
@@ -159,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
registered, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled, reachable);
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (route == StructureLookupRoute.UNKNOWN) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
return;
@@ -334,7 +415,7 @@ public class CommandFind implements DirectorExecutor {
}
Set<String> configuredImportUrls = normalizedImportUrls(
engine.getDimension().getDatapackImports());
DatapackIngestService.configuredImports(engine.getDimension()));
return new StructureExclusionReport(
List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()),
configuredImportUrls, registeredExcluded, configuredUnregistered,
@@ -598,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
@@ -623,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
public void object(
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
String object,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
boolean teleport
@@ -647,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
}
}
if (e.hasObjectPlacement(object)) {
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
@@ -657,22 +738,41 @@ public class CommandFind implements DirectorExecutor {
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
J.runEntity(target, () -> {
if (!target.isOnline()) {
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
if (!world.equals(target.getWorld())) {
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
return;
}
loadStructureDestination(target, world, commandSender, structure, at, useLocatedY);
});
}
private void loadStructureDestination(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
try {
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
}
});
} catch (Throwable error) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
}
}
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
try {
@@ -680,16 +780,41 @@ public class CommandFind implements DirectorExecutor {
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
J.runEntity(target, () -> {
BukkitPlatform.teleportAsync(target, destination);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", at.getBlockX()), MessageArgument.untrusted("y", y), MessageArgument.untrusted("value2", at.getBlockZ())));
});
J.runEntity(target, () -> completeStructureTeleport(target, world, commandSender, structure, destination));
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
}
}
private void completeStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location destination) {
if (!target.isOnline()) {
return;
}
if (!world.equals(target.getWorld())) {
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
return;
}
try {
BukkitPlatform.teleportAsync(target, destination).whenComplete((teleported, error) -> {
if (error != null) {
Iris.reportError("Could not teleport to '" + structure + "'.", error);
}
sendStructureMessage(target, commandSender, error == null && Boolean.TRUE.equals(teleported)
? IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", destination.getBlockX()),
MessageArgument.untrusted("y", destination.getBlockY()),
MessageArgument.untrusted("value2", destination.getBlockZ()))
: C.RED + "Could not teleport to " + structure + ".");
});
} catch (Throwable error) {
commandSender.sendMessage(C.RED + "Could not teleport to " + structure + ".");
Iris.reportError("Could not teleport to '" + structure + "'.", error);
}
}
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
J.runEntity(target, () -> commandSender.sendMessage(message));
}
@@ -16,40 +16,40 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.world.BukkitWorldReconciler;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.PendingWorldReplacementManager;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.IrisWorldRemovalService;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.IrisWorldStorage;
import art.arcane.iris.world.IrisWorlds;
import art.arcane.iris.world.PendingWorldReplacementManager;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.world.lifecycle.IrisWorldRemovalService;
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackDownloader;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.command.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
@@ -67,15 +67,15 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
import static art.arcane.iris.platform.bootstrap.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.IrisMessages;
import art.arcane.iris.localization.PackDownloadMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.RuntimeUiMessages;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
public class CommandIris implements DirectorExecutor {
private static final String NO_DOWNLOAD_SOURCE = "__none__";
@@ -93,8 +93,14 @@ public class CommandIris implements DirectorExecutor {
private CommandPack pack;
private CommandFind find;
private CommandDatapack datapack;
private CommandDebug debug;
VolmitSender sender = Iris.getSender();
@Director(description = "Choose your language or the server default")
public void language() {
Iris.instance.selectLanguage(sender().getS(), new String[0]);
}
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create", descriptionKey = "iris.director.commandiris.param.name_world_create")
@@ -243,6 +249,8 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
+ "verifies the replacement.");
} catch (LifecycleOperationCoordinator.BusyException busy) {
sender().sendMessage(C.YELLOW + busy.getMessage());
} catch (Throwable failure) {
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
@@ -298,9 +306,9 @@ public class CommandIris implements DirectorExecutor {
}
}
@Director(description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
public void version() {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
@Director(hidden = true, sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
debug.version(sender);
}
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
@@ -424,14 +432,6 @@ public class CommandIris implements DirectorExecutor {
return failure.getMessage();
}
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
public void debug() {
boolean to = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG, MessageArgument.untrusted("to", to)));
}
@Director(description = "Download a project.", descriptionKey = "iris.director.commandiris.director.download_project", aliases = "dl")
public void download(
@Param(name = "pack", description = "The built-in pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", defaultValue = NO_DOWNLOAD_SOURCE, customHandler = DownloadPackHandler.class)
@@ -463,7 +463,7 @@ public class CommandIris implements DirectorExecutor {
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SENDING_METRICS));
engine().printMetrics(sender());
engine().printMetrics(sender()::sendMessage);
}
@Director(description = "Reload configuration file (this is also done automatically)", descriptionKey = "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically")
@@ -0,0 +1,815 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.object.StudioOpenCoordinator;
import art.arcane.iris.studio.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
import art.arcane.iris.studio.jigsaw.JigsawStudioProjectCreator;
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEvaluation;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.structure.StructureImporter;
import art.arcane.iris.structure.VillageImporter;
import art.arcane.iris.structure.authoring.StructureKey;
import art.arcane.iris.structure.authoring.StructureWriteResult;
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.jigsaw.IrisJigsawCompatibility;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
import art.arcane.iris.structure.jigsaw.IrisJigsawThemeSet;
import art.arcane.iris.structure.jigsaw.IrisJigsawWorkcellArchetype;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.command.specialhandlers.IrisStructureHandler;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Director(name = "jigsaw", aliases = {"jig", "jgs"}, description = "Iris Jigsaw Studio tools",
origin = DirectorOrigin.PLAYER)
public class CommandJigsaw implements DirectorExecutor {
static final long DEFAULT_STUDIO_SEED = 1337L;
static final StudioOpenCoordinator.StudioOpenKind STUDIO_OPEN_KIND =
StudioOpenCoordinator.StudioOpenKind.JIGSAW;
static final Map<UUID, SessionBinding> PLAYER_PACKS = new ConcurrentHashMap<>();
private CommandJigsawPiece piece;
private CommandJigsawPool pool;
private CommandJigsawConnector connector;
private CommandJigsawVariant variant;
private CommandJigsawWorkcell workcell;
private CommandJigsawRules rules;
private CommandJigsawPreview preview;
private CommandJigsawAdopt adopt;
@Director(description = "Create and open a new atomic Jigsaw Studio project", sync = true,
origin = DirectorOrigin.PLAYER)
public void create(
@Param(description = "Pack dimension") IrisDimension dimension,
@Param(name = "key", aliases = {"structure", "name"},
description = "New key written as structures/<key>.json") String structure,
@Param(description = "planar or spatial", defaultValue = "planar",
customHandler = JigsawModeHandler.class) String mode,
@Param(description = "iris or vanilla", defaultValue = "iris",
customHandler = JigsawCompatibilityHandler.class) String compatibility,
@Param(description = "Cell width", defaultValue = "15") int width,
@Param(description = "Cell height", defaultValue = "15") int height,
@Param(description = "Cell depth", defaultValue = "15") int depth,
@Param(description = "Studio world seed", defaultValue = "1337") long seed
) {
IrisData data = requireData(dimension);
if (data == null) {
return;
}
JigsawStudioMode studioMode;
JigsawStudioCompatibilityTarget target;
JigsawStudioCellDimensions dimensions;
try {
studioMode = parseMode(mode);
target = parseCompatibility(compatibility);
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
StructureWriteResult result;
try {
result = JigsawStudioProjectCreator.create(
data.getDataFolder().toPath(),
new JigsawStudioProjectCreator.Options(structure, studioMode, target, dimensions));
} catch (IOException | RuntimeException exception) {
Iris.reportError("Failed to create Jigsaw Studio project '" + structure + "'.", exception);
sendError("Jigsaw project creation failed: " + exception.getMessage());
return;
}
if (!result.successful()) {
sendError("Jigsaw project was not created: " + result.status());
if (!result.conflicts().isEmpty()) {
sendError(result.conflicts().getFirst().relativePath() + ": "
+ result.conflicts().getFirst().reason());
}
return;
}
try {
JigsawStudioService.clearAutosaveHistory(
data.getDataFolder().toPath(),
structure);
} catch (IOException exception) {
Iris.reportError("Failed to clear stale Jigsaw Studio history for '" + structure + "'.", exception);
sendError("Jigsaw project was created, but stale autosave history could not be cleared: "
+ exception.getMessage());
return;
}
data.invalidateStructureResources();
sender().sendMessage(C.GREEN + "Created Jigsaw project '" + structure + "' atomically.");
open(dimension, structure, seed);
}
@Director(description = "Import a registered vanilla or datapack jigsaw graph into Iris Studio",
aliases = {"import", "import-vanilla"}, sync = true, origin = DirectorOrigin.PLAYER)
public void convert(
@Param(description = "Pack dimension") IrisDimension dimension,
@Param(description = "Registered structure key such as minecraft:village_plains") String source,
@Param(description = "New Iris structure key, or auto", defaultValue = "auto") String target,
@Param(description = "Studio world seed", defaultValue = "1337") long seed
) {
if (studioIsActiveOrOpening()) {
sendError("Close the active or opening Jigsaw Studio before importing another graph.");
return;
}
IrisData data = requireData(dimension);
if (data == null) {
return;
}
NamespacedKey sourceKey;
String targetKey;
try {
sourceKey = parseRegisteredStructureKey(source);
targetKey = resolveConversionTarget(sourceKey, target);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
VillageImporter.Result result = VillageImporter.importVillage(
data,
sourceKey,
targetKey,
StructureImporter.Mode.ADD_ONLY);
if (!result.success()) {
sendError(conversionFailureMessage(result));
return;
}
data.invalidateStructureResources();
VolmitSender commandSender = sender();
commandSender.sendMessage(C.GREEN + result.message());
CommandJigsawOpen.openProject(player(), commandSender, dimension, targetKey, seed);
}
@Director(description = "Open an existing Iris jigsaw graph in transient Studio",
aliases = {"edit", "reopen"}, sync = true, origin = DirectorOrigin.PLAYER)
public void open(
@Param(description = "Pack dimension") IrisDimension dimension,
@Param(name = "key", aliases = {"structure", "name"},
description = "Existing key loaded from structures/<key>.json",
customHandler = IrisStructureHandler.class) String structure,
@Param(description = "Studio world seed", defaultValue = "1337") long seed
) {
CommandJigsawOpen.openProject(player(), sender(), dimension, structure, seed);
}
@Director(description = "Close the active Jigsaw Studio", sync = true,
origin = DirectorOrigin.PLAYER)
public void close(
@Param(description = "Discard unsaved Studio layout changes", defaultValue = "false") boolean discard
) {
Player targetPlayer = player();
ActiveContext context = active(targetPlayer);
if (context == null) {
sendError("You do not have an active Jigsaw Studio session.");
return;
}
if (context.session().isDirty() && !discard) {
sendError("This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true.");
return;
}
JigsawStudioService studioService = JigsawStudioService.get();
UUID requestId = context.request().requestId();
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
requestId,
targetPlayer.getUniqueId(),
discard);
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
sendError(closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
? "This Jigsaw Studio is saving. Wait for the save to finish before closing it."
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
? "This Jigsaw Studio is loading a variant. Wait for it to finish before closing it."
: closeStart == JigsawStudioService.CloseStart.DIRTY
? "This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true."
: closeStart == JigsawStudioService.CloseStart.NOT_OWNER
? "This Jigsaw Studio is owned by another player session."
: "This Jigsaw Studio session is no longer active.");
return;
}
VolmitSender commandSender = sender();
try {
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
if (throwable != null || result == null || result.failureCause() != null) {
studioService.cancelClose(requestId);
Throwable failure = throwable != null ? throwable : result == null ? null : result.failureCause();
commandSender.sendMessage(C.RED + "Jigsaw Studio close failed: "
+ (failure == null ? "unknown failure" : failure.getMessage()));
return;
}
JigsawStudioActivation.deactivate(context.packKey(), requestId);
PLAYER_PACKS.remove(targetPlayer.getUniqueId(),
new SessionBinding(context.packKey(), requestId));
commandSender.sendMessage(C.GREEN + "Jigsaw Studio closed.");
}));
} catch (Throwable exception) {
studioService.cancelClose(requestId);
Iris.reportError("Failed to close Jigsaw Studio '" + context.request().structureKey() + "'.", exception);
sendError("Jigsaw Studio close failed: " + exception.getMessage());
}
}
@Director(name = "delete", aliases = "remove", description = "Delete the active owned jigsaw project",
sync = true, origin = DirectorOrigin.PLAYER)
public void delete(
@Param(description = "Confirm permanent project deletion", defaultValue = "false") boolean confirm
) {
if (requireActive() == null) {
return;
}
if (!confirm) {
sendError("Project deletion removes every resource owned by this jigsaw. Run again with confirm=true.");
return;
}
JigsawStudioService.get().deleteProject(player());
}
@Director(description = "Show active Jigsaw Studio and dynamic evaluation state", sync = true,
origin = DirectorOrigin.PLAYER)
public void status() {
ActiveContext context = requireActive();
if (context == null) {
return;
}
JigsawStudioLayout layout = context.session().layout();
String selection = context.session().selectedBayId().orElse("none");
JigsawStudioVariant activeVariant = "none".equals(selection)
? null
: context.session().activeVariant(selection).orElse(null);
JigsawStudioBay selectedBay = "none".equals(selection) ? null : layout.get(selection);
JigsawStudioCellDimensions selectedDimensions = selectedBay == null
? null
: selectedBay.bounds().dimensions();
Optional<JigsawStudioGraphEvaluation> evaluation = JigsawStudioService.get().evaluation(player());
sender().sendMessage(C.AQUA + "Jigsaw Studio: " + context.request().structureKey());
sender().sendMessage(C.GRAY + "mode=" + layout.mode()
+ ", compatibility=" + context.request().compatibilityTarget()
+ ", defaultCell=" + layout.cellDimensions().width() + "x"
+ layout.cellDimensions().height() + "x" + layout.cellDimensions().depth()
+ ", workcells=" + layout.bays().size()
+ ", variants=" + layout.variantCatalog().size());
sender().sendMessage(C.GRAY + "selected=" + selection
+ ", activeVariant=" + (activeVariant == null ? "none" : activeVariant.pieceKey())
+ ", size=" + (selectedDimensions == null ? "none" : selectedDimensions.width() + "x"
+ selectedDimensions.height() + "x" + selectedDimensions.depth())
+ ", enabled=" + (selectedBay == null ? "n/a" : selectedBay.enabled())
+ ", autosavePending=" + context.session().isDirty());
if (evaluation.isPresent()) {
JigsawStudioGraphEvaluation current = evaluation.get();
sender().sendMessage(C.GRAY + "evaluation=" + current.state()
+ ", seed=" + current.seed()
+ ", theme=" + (current.selectedTheme().isBlank() ? "unthemed" : current.selectedTheme())
+ ", pieces=" + current.pieceCount()
+ ", detail=" + current.detail());
} else {
sender().sendMessage(C.GRAY + "evaluation=PENDING, seed=" + DEFAULT_STUDIO_SEED);
}
}
@Director(description = "Open the Jigsaw Studio workcell and variant menu", sync = true,
origin = DirectorOrigin.PLAYER)
public void menu() {
if (requireActive() != null) {
JigsawStudioService.get().openControlMenu(player());
}
}
@Director(description = "Select the Jigsaw Studio workcell containing you", sync = true,
origin = DirectorOrigin.PLAYER)
public void select() {
ActiveContext context = requireActive();
if (context == null) {
return;
}
Location location = player().getLocation();
JigsawStudioBay bay = context.session().layout().findAt(
location.getBlockX(), location.getBlockY(), location.getBlockZ());
if (bay == null) {
sendError("Stand inside a Jigsaw Studio workcell before selecting it.");
return;
}
context.session().selectBay(bay.stableId());
sender().sendMessage(C.GREEN + "Selected " + bay.stableId() + " (" + bay.kind() + ").");
}
@Director(name = "bounds", aliases = {"cell", "resize"},
description = "Set the selected Studio workcell capacity",
sync = true, origin = DirectorOrigin.PLAYER)
public void bounds(
@Param(description = "Cell width") int width,
@Param(description = "Cell height") int height,
@Param(description = "Cell depth") int depth
) {
ActiveContext context = requireActive();
if (context == null) {
return;
}
JigsawStudioBay selected = selectedBay(context);
if (selected == null) {
sendError("Select a Jigsaw Studio workcell before changing its capacity.");
return;
}
JigsawStudioCellDimensions dimensions;
try {
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
if (dimensions.equals(selected.capacity())) {
sender().sendMessage(C.YELLOW + "Workcell capacity is unchanged.");
return;
}
JigsawStudioService.get().updateWorkcellDimensions(player(), selected.stableId(), dimensions);
}
@Director(description = "Flush the automatic save for a workcell now", sync = true,
origin = DirectorOrigin.PLAYER)
public void save(
@Param(description = "Workcell ID or selected", defaultValue = "selected") String bay
) {
ActiveContext context = requireActive();
if (context == null) {
return;
}
if ("selected".equalsIgnoreCase(bay)) {
JigsawStudioBay selected = selectedBay(context);
if (selected == null) {
sendError("Select a Jigsaw Studio workcell before flushing its autosave.");
return;
}
JigsawStudioService.get().flushAutosave(player(), selected.stableId());
return;
}
JigsawStudioService.get().flushAutosave(player(), bay);
}
@Director(name = "goto", aliases = "teleport", description = "Select and teleport to a Studio workcell",
sync = true, origin = DirectorOrigin.PLAYER)
public void gotoBay(
@Param(description = "Workcell ID") String bay
) {
if (requireActive() != null) {
JigsawStudioService.get().teleportTo(player(), bay);
}
}
@Director(description = "Enable or disable Jigsaw Studio particles", sync = true,
origin = DirectorOrigin.PLAYER)
public void particles(
@Param(description = "Whether particles are visible") boolean visible
) {
if (requireActive() != null) {
JigsawStudioService.get().setParticles(player(), visible);
}
}
@Director(description = "Export the clean active graph as a vanilla datapack", sync = true,
origin = DirectorOrigin.PLAYER)
public void export(
@Param(description = "Datapack namespace", defaultValue = "iris") String namespace,
@Param(description = "Output name under the Studio exports folder", defaultValue = "jigsaw-export")
String output,
@Param(description = "directory or zip", defaultValue = "zip") String format,
@Param(description = "Replace an existing export", defaultValue = "false") boolean replace
) {
ActiveContext context = requireActive();
if (context == null) {
return;
}
if (context.session().isDirty()) {
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
return;
}
VanillaJigsawExportFormat exportFormat;
try {
exportFormat = CommandJigsawExports.parseFormat(format);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
Path exportRoot = new File(Iris.service(StudioSVC.class).getWorkspaceFolder(), "exports")
.toPath().toAbsolutePath().normalize();
Path destination;
try {
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
VanillaJigsawExportRequest request;
try {
request = VanillaJigsawExportRequest.builder(
VanillaJigsawExportSource.forData(
context.request().source(), context.request().structureKey()), destination)
.namespace(namespace)
.resourcePath(context.request().structureKey())
.format(exportFormat)
.replaceExisting(replace)
.build();
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
}
Player targetPlayer = player();
UUID playerId = targetPlayer.getUniqueId();
UUID requestId = context.request().requestId();
JigsawStudioService studioService = JigsawStudioService.get();
JigsawStudioService.ExportStart exportStart = studioService.tryBeginExport(requestId, playerId);
if (exportStart != JigsawStudioService.ExportStart.STARTED) {
sendError(CommandJigsawExports.exportStartError(exportStart));
return;
}
if (!CommandJigsawExports.beginExport(playerId, destination)) {
studioService.finishExport(requestId);
sendError("A vanilla Jigsaw export for this player or output is already in progress.");
return;
}
CommandJigsawExports.ExportLease lease = new CommandJigsawExports.ExportLease(() -> {
CommandJigsawExports.finishExport(playerId, destination);
studioService.finishExport(requestId);
});
CommandJigsawExports.ExportOperation operation = new CommandJigsawExports.ExportOperation(
targetPlayer,
sender(),
requestId,
context.request().structureKey(),
request,
lease);
sender().sendMessage(C.GRAY + "Exporting '" + operation.structureKey()
+ "' in the background to " + destination + "...");
try {
J.a(() -> CommandJigsawExports.runExport(operation));
} catch (RuntimeException exception) {
operation.lease().release();
Iris.reportError("Failed to schedule vanilla Jigsaw export for '"
+ operation.structureKey() + "'.", exception);
sendError("Vanilla export could not be scheduled: " + exception.getMessage());
}
}
static boolean studioIsActiveOrOpening() {
return JigsawStudioActivation.activeOwnerId() != null
|| JigsawStudioActivation.openingOwnerId() != null;
}
static ActiveContext active(Player player) {
if (player == null) {
return null;
}
SessionBinding binding = PLAYER_PACKS.get(player.getUniqueId());
if (binding == null) {
return null;
}
ActiveContext context = context(binding);
if (context == null) {
PLAYER_PACKS.remove(player.getUniqueId(), binding);
return null;
}
return context;
}
private static ActiveContext context(SessionBinding binding) {
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
if (request == null || session == null
|| !binding.requestId().equals(request.requestId())
|| !request.requestId().equals(session.sessionId())) {
return null;
}
return new ActiveContext(binding.packKey(), request, session);
}
static ActiveContext findDirtyContext() {
for (SessionBinding binding : PLAYER_PACKS.values()) {
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
if (request != null && session != null
&& binding.requestId().equals(request.requestId())
&& session.isDirty()) {
return new ActiveContext(binding.packKey(), request, session);
}
}
return null;
}
private ActiveContext requireActive() {
ActiveContext context = active(player());
if (context == null) {
sendError("Open a Jigsaw Studio project first.");
}
return context;
}
private IrisData requireData(IrisDimension dimension) {
return requireData(dimension, sender());
}
static IrisData requireData(IrisDimension dimension, VolmitSender commandSender) {
IrisData data = dimension == null ? null : dimension.getLoader();
if (data == null) {
sendError(commandSender, "Could not resolve the requested Iris pack dimension.");
}
return data;
}
private void sendError(String message) {
sender().sendMessage(C.RED + message);
}
static void sendError(VolmitSender commandSender, String message) {
commandSender.sendMessage(C.RED + message);
}
private static JigsawStudioMode parseMode(String value) {
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "planar", "2d", "planar_jigsaw" -> JigsawStudioMode.PLANAR_JIGSAW;
case "spatial", "3d", "spatial_jigsaw" -> JigsawStudioMode.SPATIAL_JIGSAW;
default -> throw new IllegalArgumentException("Mode must be planar or spatial.");
};
}
private static JigsawStudioCompatibilityTarget parseCompatibility(String value) {
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "extended", "iris", "iris_extended" -> JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
case "vanilla", "portable", "vanilla_portable" -> JigsawStudioCompatibilityTarget.VANILLA_PORTABLE;
default -> throw new IllegalArgumentException("Compatibility must be iris or vanilla.");
};
}
static JigsawStudioCompatibilityTarget compatibilityOf(IrisStructure structure) {
return structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE
? JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
: JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
}
static NamespacedKey parseRegisteredStructureKey(String value) {
String normalized = Objects.requireNonNull(value, "Registered structure key").trim()
.toLowerCase(Locale.ROOT);
NamespacedKey key = NamespacedKey.fromString(normalized);
if (key == null) {
throw new IllegalArgumentException(
"Registered structure source must use a valid namespace:path key.");
}
return key;
}
static String resolveConversionTarget(NamespacedKey source, String value) {
Objects.requireNonNull(source, "Registered structure key");
String normalized = Objects.requireNonNull(value, "Conversion target").trim()
.toLowerCase(Locale.ROOT);
if (normalized.isEmpty() || "auto".equals(normalized)) {
return StructureImporter.deriveName(source);
}
StructureKey target = StructureKey.parse(normalized, "iris");
if (!"iris".equals(target.namespace())) {
throw new IllegalArgumentException("Converted Studio targets must use the iris namespace.");
}
return target.path();
}
static String conversionFailureMessage(VillageImporter.Result result) {
String message = Objects.requireNonNull(result, "Village import result").message();
String obsoleteInstruction = "; use 'import' for single-template structures";
int instructionIndex = message.indexOf(obsoleteInstruction);
if (instructionIndex < 0) {
return message;
}
return message.substring(0, instructionIndex)
+ ". This Studio converter accepts registered jigsaw structures only.";
}
static JigsawStudioBay selectedBay(ActiveContext context) {
String selected = context.session().selectedBayId().orElse(null);
return selected == null ? null : context.session().layout().get(selected);
}
static JigsawStudioVariant selectedVariant(ActiveContext context) {
JigsawStudioBay selected = selectedBay(context);
return selected == null
? null
: context.session().activeVariant(selected.stableId()).orElse(null);
}
static boolean runGraphMutation(
Player player,
ActiveContext context,
JigsawStudioService.CommandGraphMutation mutation
) {
return JigsawStudioService.get().runCommandGraphMutation(
player,
context.request().requestId(),
mutation);
}
static JigsawStudioLayout loadPersistedLayout(ActiveContext context) throws IOException {
IrisData source = context.request().source();
source.invalidateStructureResources();
IrisStructure structure = source.load(IrisStructure.class, context.request().structureKey(), false);
if (structure == null) {
throw new IOException("The structure could not be reloaded after the graph transaction");
}
return JigsawStudioGraphMapper.map(source, structure);
}
static JigsawStudioService.CommandGraphMutationResult mappedMutationResult(
ActiveContext context,
String activateWorkcellId,
String activatePieceKey,
String message
) throws IOException {
return new JigsawStudioService.CommandGraphMutationResult(
loadPersistedLayout(context),
activateWorkcellId,
activatePieceKey,
message);
}
static String workcellIdForVariant(JigsawStudioLayout layout, String pieceKey) throws IOException {
JigsawStudioVariant variant = layout.variantCatalog().find(pieceKey).orElse(null);
if (variant == null) {
throw new IOException("Persisted variant '" + pieceKey + "' did not map to a Studio workcell");
}
return variant.archetype()
.map(archetype -> archetype.stableId())
.orElse(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
}
static PieceWorkcellResolution resolvePieceWorkcell(
JigsawStudioLayout layout,
IrisJigsawPiece piece,
IrisObject object
) {
JigsawStudioLayout activeLayout = Objects.requireNonNull(layout, "Jigsaw Studio layout");
IrisJigsawPiece activePiece = Objects.requireNonNull(piece, "Jigsaw piece");
IrisObject activeObject = Objects.requireNonNull(object, "Jigsaw piece object");
String workcellId;
IrisPosition requiredDimensions;
if (activeLayout.mode() == JigsawStudioMode.PLANAR_JIGSAW) {
IrisJigsawWorkcellArchetype modelArchetype = IrisJigsawWorkcellArchetype.fromPiece(activePiece);
workcellId = JigsawPlanarArchetype.fromModel(modelArchetype).stableId();
requiredDimensions = PlanarJigsawWorkcellResolver.canonicalDimensions(activePiece, activeObject);
} else {
workcellId = JigsawStudioLayout.SPATIAL_WORKCELL_ID;
requiredDimensions = new IrisPosition(
activeObject.getW(),
activeObject.getH(),
activeObject.getD());
}
JigsawStudioBay workcell = activeLayout.get(workcellId);
if (workcell == null) {
throw new IllegalArgumentException("Workcell '" + workcellId + "' is not configured");
}
return new PieceWorkcellResolution(workcell, requiredDimensions);
}
static String nextThemeKey(ActiveContext context) throws IOException {
IrisStructure structure = context.request().source().load(
IrisStructure.class,
context.request().structureKey(),
false);
if (structure == null) {
throw new IOException("The active jigsaw structure could not be loaded");
}
Set<String> declared = new HashSet<>();
if (structure.getThemeSets() != null) {
for (IrisJigsawThemeSet themeSet : structure.getThemeSets()) {
if (themeSet != null && themeSet.getKey() != null) {
declared.add(themeSet.getKey());
}
}
}
int candidate = 1;
while (candidate > 0) {
String key = "variant-" + candidate;
if (!declared.contains(key)) {
return key;
}
candidate = Math.incrementExact(candidate);
}
throw new IOException("Jigsaw Studio cannot allocate another numbered family key");
}
static void sendError(DirectorExecutor executor, String message) {
executor.sender().sendMessage(C.RED + message);
}
public record ActiveContext(
String packKey,
JigsawStudioActivation.Request request,
JigsawStudioSession session
) {
}
record PieceWorkcellResolution(
JigsawStudioBay workcell,
IrisPosition requiredDimensions
) {
PieceWorkcellResolution {
workcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell");
requiredDimensions = Objects.requireNonNull(requiredDimensions, "Jigsaw piece dimensions");
}
boolean fits() {
JigsawStudioCellDimensions capacity = workcell.capacity();
return requiredDimensions.getX() <= capacity.width()
&& requiredDimensions.getY() <= capacity.height()
&& requiredDimensions.getZ() <= capacity.depth();
}
}
record SessionBinding(String packKey, UUID requestId) {
}
public static final class JigsawModeHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>("planar", "spatial");
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return parseMode(input) == JigsawStudioMode.PLANAR_JIGSAW ? "planar" : "spatial";
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException(exception.getMessage());
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
public static final class JigsawCompatibilityHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>("iris", "vanilla");
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return parseCompatibility(input) == JigsawStudioCompatibilityTarget.IRIS_EXTENDED
? "iris" : "vanilla";
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException(exception.getMessage());
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -0,0 +1,423 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.authoring.StructureKey;
import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionDiagnostic;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionPlan;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionRequest;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionResult;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionService;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionStrategy;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Director(name = "adopt", description = "Inspect and apply safe ownership adoption plans",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawAdopt implements DirectorExecutor {
private static final Map<UUID, AdoptionPlanBinding> ADOPTION_PLANS = new ConcurrentHashMap<>();
private static final Map<Path, IrisStructureAdoptionService> ADOPTION_SERVICES = new ConcurrentHashMap<>();
@Director(description = "Inspect an Iris graph for safe Studio ownership adoption", sync = true,
origin = DirectorOrigin.PLAYER)
public void inspect(
@Param(description = "Pack dimension") IrisDimension dimension,
@Param(description = "Existing Iris structure key") String source,
@Param(description = "New owned Iris key, or auto", defaultValue = "auto") String target,
@Param(description = "auto, in-place, or clone", defaultValue = "auto",
customHandler = JigsawAdoptionStrategyHandler.class) String strategy
) {
VolmitSender commandSender = sender();
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
if (data == null) {
return;
}
String sourceKey;
Optional<StructureKey> targetKey;
IrisStructureAdoptionStrategy adoptionStrategy;
Path root;
try {
sourceKey = parseAdoptionSource(source);
targetKey = parseAdoptionTarget(target);
adoptionStrategy = parseAdoptionStrategy(strategy);
root = canonicalAdoptionRoot(data.getDataFolder().toPath());
} catch (IOException | IllegalArgumentException exception) {
CommandJigsaw.sendError(commandSender, "Could not inspect adoption source: " + exception.getMessage());
return;
}
Player targetPlayer = player();
UUID ownerId = targetPlayer.getUniqueId();
commandSender.sendMessage(C.GRAY + "Inspecting Jigsaw adoption source '" + sourceKey
+ "' asynchronously...");
try {
J.a(() -> runAdoptionInspect(
targetPlayer,
commandSender,
ownerId,
dimension,
data,
root,
sourceKey,
targetKey,
adoptionStrategy));
} catch (RuntimeException exception) {
Iris.reportError("Failed to schedule Jigsaw adoption inspection for '"
+ sourceKey + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection could not be scheduled: "
+ exception.getMessage());
}
}
@Director(description = "Apply one inspected adoption plan without overwriting", sync = true,
origin = DirectorOrigin.PLAYER)
public void apply(
@Param(description = "Plan UUID returned by adopt inspect",
customHandler = JigsawAdoptionPlanHandler.class) String planId
) {
VolmitSender commandSender = sender();
UUID parsedPlanId;
try {
parsedPlanId = UUID.fromString(planId);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(commandSender, "Adoption plan ID must be a UUID.");
return;
}
if (CommandJigsaw.studioIsActiveOrOpening()) {
CommandJigsaw.sendError(commandSender,
"Close the active or opening Jigsaw Studio before applying an adoption plan.");
return;
}
pruneAdoptionPlanBindings();
AdoptionPlanBinding binding = ADOPTION_PLANS.get(parsedPlanId);
UUID playerId = player().getUniqueId();
if (binding == null || !binding.ownerId().equals(playerId)) {
CommandJigsaw.sendError(commandSender, "No active adoption plan '" + parsedPlanId
+ "' belongs to your player session. Run adopt inspect again.");
return;
}
if (binding.service().plan(parsedPlanId).isEmpty()) {
ADOPTION_PLANS.remove(parsedPlanId, binding);
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId
+ "' expired or was already consumed. Run adopt inspect again.");
return;
}
if (!ADOPTION_PLANS.remove(parsedPlanId, binding)) {
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId + "' is already being applied.");
return;
}
Player targetPlayer = player();
commandSender.sendMessage(C.GRAY + "Applying adoption plan '" + parsedPlanId
+ "' asynchronously without overwrite...");
try {
J.a(() -> runAdoptionApply(targetPlayer, commandSender, parsedPlanId, binding));
} catch (RuntimeException exception) {
ADOPTION_PLANS.putIfAbsent(parsedPlanId, binding);
Iris.reportError("Failed to schedule Jigsaw adoption plan '" + parsedPlanId + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw adoption could not be scheduled: " + exception.getMessage());
}
}
private static void pruneAdoptionPlanBindings() {
for (Map.Entry<UUID, AdoptionPlanBinding> entry : ADOPTION_PLANS.entrySet()) {
AdoptionPlanBinding binding = entry.getValue();
if (binding.service().plan(entry.getKey()).isEmpty()) {
ADOPTION_PLANS.remove(entry.getKey(), binding);
}
}
}
private static void runAdoptionApply(
Player targetPlayer,
VolmitSender commandSender,
UUID planId,
AdoptionPlanBinding binding
) {
if (CommandJigsaw.studioIsActiveOrOpening()) {
ADOPTION_PLANS.putIfAbsent(planId, binding);
J.runEntity(targetPlayer, () -> CommandJigsaw.sendError(commandSender,
"Close the active or opening Jigsaw Studio before applying an adoption plan."));
return;
}
IrisStructureAdoptionResult result;
Throwable failure = null;
try {
result = binding.service().apply(binding.plan());
} catch (Throwable throwable) {
result = null;
failure = throwable;
Iris.reportError("Failed to apply Jigsaw adoption plan '" + planId + "'.", throwable);
}
IrisStructureAdoptionResult completedResult = result;
Throwable completedFailure = failure;
J.runEntity(targetPlayer, () -> finishAdoptionApply(
targetPlayer,
commandSender,
binding,
completedResult,
completedFailure));
}
private static void runAdoptionInspect(
Player targetPlayer,
VolmitSender commandSender,
UUID ownerId,
IrisDimension dimension,
IrisData data,
Path root,
String sourceKey,
Optional<StructureKey> targetKey,
IrisStructureAdoptionStrategy strategy
) {
IrisStructureAdoptionService service = null;
IrisStructureAdoptionPlan plan = null;
Throwable failure = null;
try {
IrisStructureAdoptionInputKind inputKind = adoptionInputKind(root, sourceKey);
service = ADOPTION_SERVICES.computeIfAbsent(root, IrisStructureAdoptionService::new);
plan = service.inspect(new IrisStructureAdoptionRequest(
sourceKey,
targetKey,
strategy,
inputKind));
} catch (Throwable throwable) {
failure = throwable;
Iris.reportError("Failed to inspect Jigsaw adoption source '" + sourceKey + "'.", throwable);
}
IrisStructureAdoptionService completedService = service;
IrisStructureAdoptionPlan completedPlan = plan;
Throwable completedFailure = failure;
J.runEntity(targetPlayer, () -> finishAdoptionInspect(
commandSender,
ownerId,
dimension,
data,
completedService,
completedPlan,
completedFailure));
}
private static void finishAdoptionInspect(
VolmitSender commandSender,
UUID ownerId,
IrisDimension dimension,
IrisData data,
IrisStructureAdoptionService service,
IrisStructureAdoptionPlan plan,
Throwable failure
) {
if (failure != null || service == null || plan == null) {
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
pruneAdoptionPlanBindings();
if (service.plan(plan.planId()).isPresent()) {
ADOPTION_PLANS.put(plan.planId(), new AdoptionPlanBinding(
ownerId, dimension, data, service, plan));
}
reportAdoptionPlan(commandSender, plan);
}
private static void finishAdoptionApply(
Player targetPlayer,
VolmitSender commandSender,
AdoptionPlanBinding binding,
IrisStructureAdoptionResult result,
Throwable failure
) {
if (failure != null || result == null) {
CommandJigsaw.sendError(commandSender, "Jigsaw adoption failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
reportAdoptionResult(commandSender, result);
if (!result.successful()) {
return;
}
StructureKey target = result.receipt().orElseThrow().targetStructure();
binding.data().invalidateStructureResources();
CommandJigsawOpen.openProject(targetPlayer, commandSender, binding.dimension(), target.path(), CommandJigsaw.DEFAULT_STUDIO_SEED);
}
private static void reportAdoptionPlan(VolmitSender commandSender, IrisStructureAdoptionPlan plan) {
C statusColor = plan.canApply() ? C.GREEN : C.RED;
commandSender.sendMessage(statusColor + "Adoption plan " + plan.planId() + " -> "
+ plan.disposition() + " for " + plan.targetStructure().value() + ".");
commandSender.sendMessage(C.GRAY + "Resources=" + plan.resourceCount()
+ ", bytes=" + plan.totalSourceBytes()
+ ", errors=" + plan.errorCount()
+ ", warnings=" + plan.warningCount() + ".");
for (IrisStructureAdoptionDiagnostic diagnostic : plan.diagnostics()) {
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
}
if (plan.canApply()) {
commandSender.sendMessage(C.AQUA + "Apply with /iris jigsaw adopt apply " + plan.planId());
}
}
private static void reportAdoptionResult(VolmitSender commandSender, IrisStructureAdoptionResult result) {
commandSender.sendMessage((result.successful() ? C.GREEN : C.RED)
+ "Adoption plan " + result.planId() + ": " + result.status() + ".");
for (IrisStructureAdoptionDiagnostic diagnostic : result.diagnostics()) {
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
}
}
private static C adoptionColor(IrisStructureAdoptionDiagnostic diagnostic) {
return switch (diagnostic.severity()) {
case ERROR -> C.RED;
case WARNING -> C.YELLOW;
case INFO -> C.GRAY;
};
}
static String parseAdoptionSource(String value) {
String normalized = Objects.requireNonNull(value, "Adoption source").trim()
.toLowerCase(Locale.ROOT);
StructureKey source = StructureKey.parse(normalized, "iris");
if (!"iris".equals(source.namespace())) {
throw new IllegalArgumentException("Adoption sources must use the iris namespace.");
}
return source.path();
}
static Optional<StructureKey> parseAdoptionTarget(String value) {
String normalized = Objects.requireNonNull(value, "Adoption target").trim()
.toLowerCase(Locale.ROOT);
if (normalized.isEmpty() || "auto".equals(normalized)) {
return Optional.empty();
}
StructureKey target = StructureKey.parse(normalized, "iris");
if (!"iris".equals(target.namespace())) {
throw new IllegalArgumentException("Adoption targets must use the iris namespace.");
}
return Optional.of(target);
}
private static IrisStructureAdoptionStrategy parseAdoptionStrategy(String value) {
return switch (Objects.requireNonNull(value, "Adoption strategy").trim().toLowerCase(Locale.ROOT)) {
case "auto" -> IrisStructureAdoptionStrategy.AUTO;
case "in-place", "in_place", "inplace", "claim" -> IrisStructureAdoptionStrategy.IN_PLACE;
case "clone", "copy" -> IrisStructureAdoptionStrategy.CLONE;
default -> throw new IllegalArgumentException("Adoption strategy must be auto, in-place, or clone.");
};
}
static IrisStructureAdoptionInputKind adoptionInputKind(Path packRoot, String source) throws IOException {
Path root = canonicalAdoptionRoot(packRoot);
StructureKey sourceKey = new StructureKey("iris", parseAdoptionSource(source));
Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(sourceKey);
if (!Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
return IrisStructureAdoptionInputKind.UNOWNED_IRIS;
}
if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Ownership manifest is not a regular file: " + manifestPath);
}
StructureOwnershipManifest manifest;
try {
manifest = StructureOwnershipManifest.fromJson(Files.readAllBytes(manifestPath));
} catch (RuntimeException exception) {
throw new IOException("Invalid ownership manifest at " + manifestPath, exception);
}
if (!manifest.structure().equals(sourceKey)) {
throw new IOException("Ownership manifest at " + manifestPath
+ " belongs to " + manifest.structure().value());
}
return manifest.provenance().origin() == StructureOwnershipManifest.Origin.MANAGED_DATAPACK
? IrisStructureAdoptionInputKind.MANAGED_DATAPACK
: IrisStructureAdoptionInputKind.UNOWNED_IRIS;
}
private static Path canonicalAdoptionRoot(Path packRoot) throws IOException {
Path normalized = Objects.requireNonNull(packRoot, "Adoption pack root").toAbsolutePath().normalize();
return Files.exists(normalized, LinkOption.NOFOLLOW_LINKS) ? normalized.toRealPath() : normalized;
}
private record AdoptionPlanBinding(
UUID ownerId,
IrisDimension dimension,
IrisData data,
IrisStructureAdoptionService service,
IrisStructureAdoptionPlan plan
) {
}
public static final class JigsawAdoptionStrategyHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>("auto", "in-place", "clone");
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return switch (parseAdoptionStrategy(input)) {
case AUTO -> "auto";
case IN_PLACE -> "in-place";
case CLONE -> "clone";
};
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException(exception.getMessage());
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
public static final class JigsawAdoptionPlanHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>();
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return UUID.fromString(input).toString();
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException("Adoption plan ID must be a UUID.");
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -0,0 +1,83 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Jigsaw;
@Director(name = "connector", description = "Inspect and configure targeted jigsaw connectors",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawConnector implements DirectorExecutor {
@Director(description = "Set the Iris-only channel on the targeted jigsaw marker", sync = true,
origin = DirectorOrigin.PLAYER)
public void channel(
@Param(description = "Exact channel, or none to clear") String channel
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
Block target = player().getTargetBlockExact(8);
if (target == null || !(target.getBlockData() instanceof Jigsaw)) {
CommandJigsaw.sendError(this, "Look directly at a jigsaw marker within eight blocks.");
return;
}
JigsawStudioBay bay = context.session().layout().findAt(
target.getX(), target.getY(), target.getZ());
JigsawStudioVariant activeVariant = bay == null
? null
: context.session().activeVariant(bay.stableId()).orElse(null);
if (activeVariant == null) {
CommandJigsaw.sendError(this, "The targeted marker must be inside a workcell with an active variant.");
return;
}
if (!activeVariant.owned()) {
CommandJigsaw.sendError(this, "The active variant is read-only. Adopt it into this project before editing it.");
return;
}
IrisObject object = context.request().source().getObjectLoader().load(activeVariant.objectKey());
if (object == null) {
CommandJigsaw.sendError(this, "The active variant object '" + activeVariant.objectKey() + "' is missing.");
return;
}
IrisPosition canonicalPosition = new IrisPosition(
target.getX() - bay.bounds().originX(),
target.getY() - bay.bounds().originY(),
target.getZ() - bay.bounds().originZ());
IrisPosition position;
try {
position = activeVariant.canonicalToSourcePosition(
canonicalPosition,
new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()));
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, "Connector channel update failed: " + exception.getMessage());
return;
}
String normalized = channel == null || channel.isBlank() || "none".equalsIgnoreCase(channel)
? "none"
: channel.trim();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.updateConnectorChannel(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
activeVariant.pieceKey(),
position,
channel);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted connector channel '" + normalized + "' on piece '"
+ activeVariant.pieceKey() + "' at " + position.getX() + "," + position.getY()
+ "," + position.getZ() + ".");
});
}
}
@@ -0,0 +1,159 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import org.bukkit.entity.Player;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
final class CommandJigsawExports {
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
private CommandJigsawExports() {
}
static String exportStartError(JigsawStudioService.ExportStart exportStart) {
Objects.requireNonNull(exportStart, "Jigsaw Studio export start status");
return switch (exportStart) {
case NOT_ACTIVE -> "The active Jigsaw Studio is no longer available.";
case NOT_OWNER -> "Only the Jigsaw Studio owner can export this project.";
case DIRTY -> "Wait for the pending autosave or discard the edits before exporting the on-disk graph.";
case CLOSING -> "The active Jigsaw Studio is closing and cannot be exported.";
case SAVE_IN_PROGRESS -> "Wait for the current Jigsaw Studio save to finish before exporting.";
case OPERATION_IN_PROGRESS -> "Wait for the current Jigsaw Studio operation to finish before exporting.";
case IN_PROGRESS -> "A Jigsaw Studio export is already in progress.";
case STARTED -> throw new IllegalArgumentException("STARTED is not an export failure");
};
}
static boolean beginExport(UUID playerId, Path destination) {
Objects.requireNonNull(playerId);
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
if (!EXPORTING_PLAYERS.add(playerId)) {
return false;
}
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
return true;
}
EXPORTING_PLAYERS.remove(playerId);
return false;
}
static void finishExport(UUID playerId, Path destination) {
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
}
static void runExport(ExportOperation operation) {
VanillaJigsawExportResult result = null;
Throwable failure = null;
try {
result = new VanillaJigsawDatapackExporter().export(operation.request());
} catch (Throwable throwable) {
failure = throwable;
Iris.reportError("Vanilla Jigsaw export failed for '" + operation.structureKey() + "'.", throwable);
} finally {
operation.lease().release();
}
VanillaJigsawExportResult completedResult = result;
Throwable completedFailure = failure;
Runnable response = () -> reportExport(operation, completedResult, completedFailure);
J.runEntity(operation.player(), response);
}
private static void reportExport(
ExportOperation operation,
VanillaJigsawExportResult result,
Throwable failure
) {
if (failure != null || result == null) {
operation.sender().sendMessage(C.RED + "Background vanilla export for '"
+ operation.structureKey() + "' failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
for (VanillaJigsawExportDiagnostic diagnostic : result.diagnostics()) {
C color = diagnostic.isBlocking() ? C.RED : C.YELLOW;
operation.sender().sendMessage(color.toString() + diagnostic.code() + ": " + diagnostic.message());
}
CommandJigsaw.ActiveContext current = CommandJigsaw.active(operation.player());
boolean sameSession = current != null
&& current.request().requestId().equals(operation.requestId());
String contextLabel = sameSession ? "Vanilla export" : "Background vanilla export for '"
+ operation.structureKey() + "'";
operation.sender().sendMessage((result.isSuccess() ? C.GREEN : C.RED) + contextLabel + " "
+ result.status() + ": " + result.output());
}
static VanillaJigsawExportFormat parseFormat(String value) {
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "zip" -> VanillaJigsawExportFormat.ZIP;
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
};
}
static Path resolveExportDestination(
Path exportRoot,
String output,
VanillaJigsawExportFormat format
) {
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
String name = requested.trim();
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
}
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
? name + ".zip" : name;
Path destination = root.resolve(fileName).normalize();
if (destination.equals(root) || !root.equals(destination.getParent())) {
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
}
return destination;
}
record ExportOperation(
Player player,
VolmitSender sender,
UUID requestId,
String structureKey,
VanillaJigsawExportRequest request,
ExportLease lease
) {
}
static final class ExportLease {
private final Runnable release;
private final AtomicBoolean released;
ExportLease(Runnable release) {
this.release = Objects.requireNonNull(release, "Jigsaw export lease release action");
this.released = new AtomicBoolean();
}
void release() {
if (released.compareAndSet(false, true)) {
release.run();
}
}
}
}
@@ -0,0 +1,251 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
import art.arcane.iris.studio.object.StudioOpenCoordinator;
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.UUID;
import java.util.concurrent.CompletionException;
final class CommandJigsawOpen {
private CommandJigsawOpen() {
}
static void openProject(
Player targetPlayer,
VolmitSender commandSender,
IrisDimension dimension,
String structure,
long seed
) {
UUID playerId = targetPlayer.getUniqueId();
UUID ownerId = JigsawStudioActivation.activeOwnerId();
if (ownerId != null && !ownerId.equals(playerId)) {
CommandJigsaw.sendError(commandSender, "Jigsaw Studio is already owned by another player session.");
return;
}
UUID openingOwner = JigsawStudioActivation.openingOwnerId();
if (openingOwner != null) {
CommandJigsaw.sendError(commandSender, openingOwner.equals(playerId)
? "Your Jigsaw Studio is already opening."
: "Jigsaw Studio is already opening for another player.");
return;
}
CommandJigsaw.ActiveContext previous = CommandJigsaw.active(targetPlayer);
if (ownerId != null && previous == null) {
CommandJigsaw.sendError(commandSender, "Your active Jigsaw Studio binding is unavailable. Close it before opening another project.");
return;
}
CommandJigsaw.ActiveContext dirty = CommandJigsaw.findDirtyContext();
if (dirty != null) {
CommandJigsaw.sendError(commandSender,
"The current Jigsaw Studio is waiting for autosave. Wait for it to finish or close with discard=true.");
return;
}
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
if (data == null) {
return;
}
IrisStructure irisStructure = data.load(IrisStructure.class, structure, false);
if (irisStructure == null) {
CommandJigsaw.sendError(commandSender, "No Iris jigsaw structure '" + structure + "' exists in this pack.");
return;
}
JigsawStudioLayout layout;
try {
layout = JigsawStudioGraphMapper.map(data, irisStructure);
} catch (RuntimeException exception) {
Iris.reportError("Failed to map Jigsaw Studio graph '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Could not map this jigsaw graph: " + exception.getMessage());
return;
}
String packKey = dimension.getLoadKey();
if (!JigsawStudioActivation.tryBeginOpen(playerId)) {
CommandJigsaw.sendError(commandSender,
"Jigsaw Studio began opening for another player. Try again after it finishes.");
return;
}
JigsawStudioService studioService = JigsawStudioService.get();
UUID closingRequestId = previous == null ? null : previous.request().requestId();
if (closingRequestId != null) {
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
closingRequestId,
playerId,
false);
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
JigsawStudioActivation.finishOpen(playerId);
CommandJigsaw.sendError(commandSender, closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
? "The current Jigsaw Studio is saving. Wait for it to finish before opening another project."
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
? "The current Jigsaw Studio is loading a variant. Wait for it to finish before opening another project."
: closeStart == JigsawStudioService.CloseStart.DIRTY
? "The current Jigsaw Studio is waiting for autosave. Wait for it to finish before opening another project."
: "The current Jigsaw Studio could not be reserved for replacement.");
return;
}
}
JigsawStudioActivation.StagedActivation staged;
try {
staged = JigsawStudioActivation.stage(
packKey,
structure,
layout.mode(),
CommandJigsaw.compatibilityOf(irisStructure),
layout.cellDimensions(),
data,
layout,
playerId,
closingRequestId);
} catch (RuntimeException exception) {
JigsawStudioActivation.finishOpen(playerId);
studioService.cancelClose(closingRequestId);
Iris.reportError("Failed to stage Jigsaw Studio for '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio staging failed: " + exception.getMessage());
return;
}
commandSender.sendMessage(C.GREEN + "Opening transient Jigsaw Studio for '" + structure + "' with "
+ layout.bays().size() + " workcells.");
try {
Iris.service(StudioSVC.class).openTracked(
commandSender,
seed,
packKey,
CommandJigsaw.STUDIO_OPEN_KIND,
() -> beginStagedOpen(staged),
world -> finishOpen(targetPlayer, world, staged, studioService))
.whenComplete((result, throwable) -> J.s(() -> finishOpenAttempt(
commandSender,
playerId,
structure,
closingRequestId,
studioService,
staged,
result,
throwable)));
} catch (Throwable exception) {
JigsawStudioActivation.finishOpen(playerId);
studioService.cancelClose(closingRequestId);
JigsawStudioActivation.rollback(staged);
Iris.reportError("Failed to open Jigsaw Studio for '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + exception.getMessage());
}
}
private static void beginStagedOpen(JigsawStudioActivation.StagedActivation staged) {
if (!JigsawStudioActivation.beginStagedGeneration(staged)) {
throw new IllegalStateException("The staged Jigsaw Studio activation is no longer current");
}
}
private static void finishOpenAttempt(
VolmitSender commandSender,
UUID playerId,
String structureKey,
UUID closingRequestId,
JigsawStudioService studioService,
JigsawStudioActivation.StagedActivation staged,
StudioOpenCoordinator.StudioOpenResult result,
Throwable throwable
) {
JigsawStudioActivation.finishOpen(playerId);
if (throwable == null && result != null) {
studioService.cancelClose(closingRequestId);
return;
}
studioService.cancelClose(closingRequestId);
JigsawStudioActivation.rollback(staged);
Throwable failure = unwrapCompletionFailure(throwable == null
? new IllegalStateException("Studio open completed without a result")
: throwable);
if (isExpectedOpenDenial(failure)) {
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
structureKey, failure.getMessage());
return;
}
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
}
private static void finishOpen(
Player targetPlayer,
World world,
JigsawStudioActivation.StagedActivation staged,
JigsawStudioService studioService
) {
if (world == null) {
throw new IllegalStateException("Jigsaw Studio opened without a world");
}
JigsawStudioActivation.Request request = staged.request();
JigsawStudioSession session = staged.session();
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable exception) {
Iris.reportError("Failed to apply Jigsaw Studio world rules for '" + world.getName() + "'.", exception);
}
JigsawStudioBay destination = initialBay(session.layout());
Location target = destination == null
? new Location(world, 0.5D, 66D, 0.5D)
: new Location(
world,
destination.bounds().originX() + destination.bounds().dimensions().width() / 2.0D,
destination.bounds().maxY() + 2.0D,
destination.bounds().originZ() + destination.bounds().dimensions().depth() / 2.0D);
if (destination != null) {
session.selectBay(destination.stableId());
}
if (!JigsawStudioActivation.commit(staged)) {
throw new IllegalStateException("The staged Jigsaw Studio activation could not be committed");
}
studioService.activationCommitted(world, request.requestId());
CommandJigsaw.PLAYER_PACKS.put(targetPlayer.getUniqueId(), new CommandJigsaw.SessionBinding(
request.packKey(), request.requestId()));
try {
J.runEntity(targetPlayer, () -> BukkitPlatform.teleportAsync(targetPlayer, target)
.thenRun(() -> J.runEntity(targetPlayer, () -> targetPlayer.setGameMode(GameMode.CREATIVE))));
} catch (RuntimeException exception) {
Iris.reportError("Failed to move the player to the committed Jigsaw Studio workcell.", exception);
}
}
private static JigsawStudioBay initialBay(JigsawStudioLayout layout) {
for (JigsawStudioBay bay : layout.bays()) {
if (layout.defaultVariant(bay).isPresent()) {
return bay;
}
}
return layout.bays().isEmpty() ? null : layout.bays().getFirst();
}
private static Throwable unwrapCompletionFailure(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
current = current.getCause();
}
return current;
}
static boolean isExpectedOpenDenial(Throwable failure) {
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
}
}
@@ -0,0 +1,237 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawPlanarTopology;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import org.bukkit.Location;
import java.io.IOException;
@Director(name = "piece", description = "Manage contextual Studio pieces", origin = DirectorOrigin.PLAYER)
public class CommandJigsawPiece implements DirectorExecutor {
@Director(description = "Create an owned piece from the selected Studio topology", sync = true,
origin = DirectorOrigin.PLAYER)
public void create(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "New owned piece and object key") String pieceKey,
@Param(description = "Positive selection weight", defaultValue = "1") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawPlanarTopology topology = null;
JigsawStudioBay targetWorkcell = context.session().layout().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
if (context.session().layout().mode() == JigsawStudioMode.PLANAR_JIGSAW) {
Location location = player().getLocation();
JigsawStudioBay contextual = context.session().layout().findAt(
location.getBlockX(), location.getBlockY(), location.getBlockZ());
if (contextual == null) {
contextual = CommandJigsaw.selectedBay(context);
} else {
context.session().selectBay(contextual.stableId());
}
if (contextual == null || contextual.topology().isEmpty()) {
CommandJigsaw.sendError(this, "Stand in or select a planar topology workcell before creating a variant.");
return;
}
topology = contextual.topology().get();
targetWorkcell = contextual;
}
if (targetWorkcell == null) {
CommandJigsaw.sendError(this, "The selected Jigsaw Studio workcell is no longer available.");
return;
}
JigsawPlanarTopology selectedTopology = topology;
String activationWorkcellId = targetWorkcell.stableId();
JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();
String topologyText = selectedTopology == null ? "blank spatial" : selectedTopology.name();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.createPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight,
dimensions,
selectedTopology);
return CommandJigsaw.mappedMutationResult(
context,
activationWorkcellId,
pieceKey,
"Created owned " + topologyText + " piece '" + pieceKey
+ "' and added it to pool '" + poolKey + "' atomically.");
});
}
@Director(description = "Add an existing jigsaw piece to an owned pool transactionally", sync = true,
origin = DirectorOrigin.PLAYER)
public void add(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "Existing jigsaw piece key") String pieceKey,
@Param(description = "Positive selection weight", defaultValue = "1") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
IrisJigsawPiece piece = context.request().source().getJigsawPieceLoader().load(pieceKey);
if (piece == null) {
CommandJigsaw.sendError(this, "No jigsaw piece '" + pieceKey + "' exists in this pack.");
return;
}
if (piece.getObject() == null || piece.getObject().isBlank()) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' does not declare an object.");
return;
}
IrisObject object = context.request().source().getObjectLoader().load(piece.getObject());
if (object == null) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' has no loadable object.");
return;
}
CommandJigsaw.PieceWorkcellResolution resolution;
try {
resolution = CommandJigsaw.resolvePieceWorkcell(context.session().layout(), piece, object);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' cannot be assigned to a Studio workcell: "
+ exception.getMessage());
return;
}
if (!resolution.fits()) {
IrisPosition required = resolution.requiredDimensions();
JigsawStudioCellDimensions capacity = resolution.workcell().capacity();
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' requires " + required.getX() + "x"
+ required.getY() + "x" + required.getZ() + " in " + resolution.workcell().displayName()
+ ", but that workcell capacity is " + capacity.width() + "x" + capacity.height()
+ "x" + capacity.depth() + ".");
return;
}
String objectKey = piece.getObject();
CommandJigsaw.runGraphMutation(player(), context, () -> {
if (!JigsawStudioGraphEditor.ownsPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
pieceKey,
objectKey)) {
throw new IOException("Piece add accepts only piece/object resources already owned by this project; "
+ "use piece create for a new owned piece");
}
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.addPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight);
if (!update.changed()) {
throw new IOException("The owned pool is missing or already references '" + pieceKey + "'");
}
JigsawStudioLayout layout = CommandJigsaw.loadPersistedLayout(context);
String activationWorkcellId = CommandJigsaw.workcellIdForVariant(layout, pieceKey);
return new JigsawStudioService.CommandGraphMutationResult(
layout,
activationWorkcellId,
pieceKey,
"Added '" + pieceKey + "' to pool '" + poolKey + "' in one graph transaction.");
});
}
@Director(description = "Remove the selected piece from an owned pool transactionally", sync = true,
origin = DirectorOrigin.PLAYER)
public void remove(
@Param(description = "Owned pool resource key") String poolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before removing its pool entry.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.removePiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey);
if (!update.changed()) {
throw new IOException("The owned pool does not reference selected piece '" + pieceKey + "'");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Removed '" + pieceKey + "' from pool '" + poolKey + "' in one graph transaction.");
});
}
@Director(description = "Allow or forbid cardinal rotation of the selected piece", sync = true,
origin = DirectorOrigin.PLAYER)
public void rotatable(
@Param(description = "Whether the selected piece may rotate") boolean rotatable
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing rotation.");
return;
}
if (!rotatable
&& context.request().compatibilityTarget()
== JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
CommandJigsaw.sendError(this, "Vanilla-portable pieces must remain rotatable.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.updateRotatable(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
pieceKey,
rotatable);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted piece '" + pieceKey + "' rotatable=" + rotatable + ".");
});
}
@Director(description = "Resize the selected piece object exactly to workcell capacity", sync = true,
origin = DirectorOrigin.PLAYER)
public void expand() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay workcell = CommandJigsaw.selectedBay(context);
if (workcell == null) {
CommandJigsaw.sendError(this, "Select a workcell before expanding its active variant.");
return;
}
JigsawStudioService.get().expandVariantToCell(player(), workcell.stableId());
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "pool", description = "Manage owned jigsaw pools", origin = DirectorOrigin.PLAYER)
public class CommandJigsawPool implements DirectorExecutor {
@Director(description = "Create an empty owned jigsaw pool", sync = true,
origin = DirectorOrigin.PLAYER)
public void create(
@Param(description = "New owned pool resource key") String poolKey,
@Param(description = "Owned fallback pool key, or none", defaultValue = "none")
String fallbackPoolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.createPool(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
fallback);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Created owned pool '" + poolKey + "' with fallback '"
+ (fallback.isBlank() ? "none" : fallback) + "'.");
});
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "preview", description = "Assemble a read-only jigsaw preview",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawPreview implements DirectorExecutor {
@Director(name = "goto", aliases = "teleport", description = "Teleport to the live seed-1337 preview",
sync = true, origin = DirectorOrigin.PLAYER)
public void gotoPreview() {
JigsawStudioService.get().goToPreview(player());
}
@Director(description = "Assemble a deterministic preview at your location", sync = true,
origin = DirectorOrigin.PLAYER)
public void assemble(
@Param(description = "Assembly seed", defaultValue = "1337") long seed
) {
JigsawStudioService.get().previewStudio(player(), seed);
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.studio.jigsaw.JigsawStudioStructureEditor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "rules", aliases = "rule", description = "Manage structure-wide jigsaw rules",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawRules implements DirectorExecutor {
@Director(description = "Set maximum expansion depth and horizontal radius", sync = true,
origin = DirectorOrigin.PLAYER)
public void limits(
@Param(description = "Maximum expansion depth") int maxDepth,
@Param(description = "Maximum radius in chunks") int maxSizeChunks
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
if (context.request().compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
&& (maxDepth > 20 || maxSizeChunks > 8)) {
CommandJigsaw.sendError(this, "Vanilla-portable rules require maxDepth <= 20 and maxSizeChunks <= 8.");
return;
}
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioStructureEditor.updateLimits(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
maxDepth,
maxSizeChunks);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted jigsaw limits: maxDepth=" + maxDepth
+ ", maxSizeChunks=" + maxSizeChunks + ".");
});
}
@Director(description = "Set or clear one owned pool's direct fallback", sync = true,
origin = DirectorOrigin.PLAYER)
public void fallback(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "Owned fallback pool key, or none") String fallbackPoolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.updateFallback(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
fallback);
if (!update.changed()) {
return CommandJigsaw.mappedMutationResult(context, "", "", "Pool fallback is unchanged.");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted pool '" + poolKey + "' fallback as '"
+ (fallback.isBlank() ? "none" : fallback) + "'.");
});
}
}
@@ -0,0 +1,168 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.IOException;
@Director(name = "variant", description = "Manage selected jigsaw variants", origin = DirectorOrigin.PLAYER)
public class CommandJigsawVariant implements DirectorExecutor {
@Director(description = "Set the selected piece weight in a pool", sync = true,
origin = DirectorOrigin.PLAYER)
public void weight(
@Param(description = "Pool resource key") String poolKey,
@Param(description = "Positive selection weight") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing its weight.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.WeightUpdate update = JigsawStudioPoolEditor.updateWeight(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight);
if (!update.changed()) {
throw new IOException("Pool '" + poolKey + "' does not reference selected piece '"
+ pieceKey + "'");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Updated " + update.changedEntries() + " pool entry weight atomically in "
+ update.poolPath() + ".");
});
}
@Director(description = "Resize only the active variant object within workcell capacity", sync = true,
origin = DirectorOrigin.PLAYER)
public void resize(
@Param(description = "Variant width") int width,
@Param(description = "Variant height") int height,
@Param(description = "Variant depth") int depth
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resizing it.");
return;
}
JigsawStudioCellDimensions dimensions;
try {
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
JigsawStudioService.get().resizeVariant(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
dimensions);
}
@Director(description = "Set the active variant's author-facing label", sync = true,
origin = DirectorOrigin.PLAYER)
public void label(
@Param(description = "Display label; quote labels containing spaces") String displayName
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before renaming it.");
return;
}
JigsawStudioService.get().updateVariantDisplayName(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
displayName);
}
@Director(name = "label-reset", aliases = "reset-label",
description = "Reset the active variant to its resource-key label", sync = true,
origin = DirectorOrigin.PLAYER)
public void labelReset() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resetting its label.");
return;
}
JigsawStudioService.get().updateVariantDisplayName(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
"");
}
@Director(description = "Duplicate the active variant in only this workcell", sync = true,
origin = DirectorOrigin.PLAYER)
public void duplicate() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null || CommandJigsaw.selectedVariant(context) == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before duplicating it.");
return;
}
JigsawStudioService.get().createVariant(player(), selected.stableId(), true);
}
@Director(name = "duplicate-family", aliases = "family",
description = "Duplicate every enabled workcell's active variant as one coherent family",
sync = true, origin = DirectorOrigin.PLAYER)
public void duplicateFamily(
@Param(description = "Family key or next", defaultValue = "next") String themeKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String selectedTheme = themeKey;
if ("next".equalsIgnoreCase(themeKey)) {
try {
selectedTheme = CommandJigsaw.nextThemeKey(context);
} catch (IOException | ArithmeticException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
}
JigsawStudioService.get().duplicateActiveFamily(player(), selectedTheme);
}
}
@@ -0,0 +1,80 @@
package art.arcane.iris.command;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "workcell", aliases = "cell", description = "Manage the selected Studio workcell",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawWorkcell implements DirectorExecutor {
@Director(description = "Set capacity without resizing any variant object", sync = true,
origin = DirectorOrigin.PLAYER)
public void capacity(
@Param(description = "Capacity width") int width,
@Param(description = "Capacity height") int height,
@Param(description = "Capacity depth") int depth
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before changing its capacity.");
return;
}
JigsawStudioCellDimensions dimensions;
try {
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
JigsawStudioService.get().updateWorkcellDimensions(
player(),
selected.stableId(),
dimensions);
}
@Director(description = "Set the selected workcell's author-facing label", sync = true,
origin = DirectorOrigin.PLAYER)
public void label(
@Param(description = "Display label; quote labels containing spaces") String displayName
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before renaming it.");
return;
}
JigsawStudioService.get().updateWorkcellDisplayName(
player(),
selected.stableId(),
displayName);
}
@Director(name = "label-reset", aliases = "reset-label",
description = "Reset the selected workcell to its canonical solver label", sync = true,
origin = DirectorOrigin.PLAYER)
public void labelReset() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before resetting its label.");
return;
}
JigsawStudioService.get().updateWorkcellDisplayName(player(), selected.stableId(), "");
}
}
@@ -16,44 +16,48 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.integration.WorldEditLink;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.studio.object.ObjectStudioActivation;
import art.arcane.iris.studio.object.StudioOpenCoordinator;
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.studio.object.ObjectSVC;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.studio.wand.WandSVC;
import art.arcane.iris.pack.IrisConverter;
import art.arcane.iris.studio.tree.TreePlausibilizeBatch;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction;
import art.arcane.iris.command.specialhandlers.ObjectHandler;
import art.arcane.iris.command.specialhandlers.ObjectScaleHandler;
import art.arcane.iris.command.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.math.DirectionBasis;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
@@ -82,11 +86,11 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.RuntimeUiMessages;
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation", descriptionKey = "iris.director.commandobject.director.iris_object_manipulation")
public class CommandObject implements DirectorExecutor {
static final Set<Material> PASTE_TRANSPARENT_BLOCKS = Set.of(
@@ -221,11 +225,19 @@ public class CommandObject implements DirectorExecutor {
//Prevent blocks being set in or bellow bedrock
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
futureBlockChanges.putIfAbsent(block, block.getBlockData());
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
futureBlockChanges.computeIfAbsent(block, target -> {
BlockData previous = target.getBlockData();
return external == null ? previous : external.captureBlockData(previous);
});
if (d instanceof IrisCustomData data) {
if (external != null && external.placeBlock(block, data.getCustom())) {
return;
}
block.setBlockData(data.getBase(), false);
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
IrisLogging.warnOnce("object-paste:external:" + data.getCustom(),
"Direct placement of %s is unavailable; only its base block was placed.", data.getCustom());
} else block.setBlockData(d, false);
}
@@ -471,9 +483,9 @@ public class CommandObject implements DirectorExecutor {
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
DirectionBasis d = DirectionBasis.closest(player.getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
cursor = cursor.expand(d.getCuboidDirection(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
@@ -527,21 +539,19 @@ public class CommandObject implements DirectorExecutor {
return;
}
if (WandSVC.isHoldingIrisWand(player)) {
Location[] g = WandSVC.getCuboid(player);
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
});
}
@@ -553,17 +563,21 @@ public class CommandObject implements DirectorExecutor {
boolean edit,
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
int rotate,
@Param(description = "The factor by which to scale the object placement", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "1")
double scale
@Param(description = "Explicit scale factor, or dimension to inherit the current Iris world's factor", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "dimension", customHandler = ObjectScaleHandler.class)
Double scale
// ,
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator
) {
IrisObject o = IrisData.loadAnyObject(object, data());
Engine placementEngine = engine();
double factor = scale == null
? placementEngine == null ? 1D : placementEngine.getDimension().getAllObjectScaleFactor()
: IrisObjectScale.requireValidFactor(scale, "scale");
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
if (scale > maxScale) {
if (factor > maxScale) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale)));
scale = maxScale;
factor = maxScale;
}
IrisObjectPlacement placement = new IrisObjectPlacement();
@@ -583,14 +597,16 @@ public class CommandObject implements DirectorExecutor {
Map<Block, BlockData> futureChanges = new HashMap<>();
if (scale != 1) {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
if (factor != 1D) {
o = scale == null ? IrisObjectScale.getFixed(o, factor)
: new IrisObjectScale().setSize(factor)
.setInterpolation(IrisObjectPlacementScaleInterpolator.TRICUBIC).get(new RNG(), o);
}
// Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o;
if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), null);
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), placed.getLoader());
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
if (!edit) {
@@ -693,7 +709,7 @@ public class CommandObject implements DirectorExecutor {
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
DirectionBasis d = DirectionBasis.closest(player.getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
@@ -16,30 +16,36 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.pack.validation.PackCompatReport;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackResourceCleanup;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.command.handlers.DimensionHandler;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.TextKey;
import art.arcane.iris.spi.IrisPlatforms;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.localization.BukkitRuntimeMessages;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
public class CommandPack implements DirectorExecutor {
/**
@@ -90,6 +96,18 @@ public class CommandPack implements DirectorExecutor {
persistValidationCache(packsRoot);
}
@Director(description = "Package a validated dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
public void pkg(
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
boolean obfuscate,
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
boolean minify
) {
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
}
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
public void cleanup(
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
@@ -235,6 +253,76 @@ public class CommandPack implements DirectorExecutor {
reportResult(s, result);
}
@Director(description = "Show pack content that cannot generate on this Minecraft version", descriptionKey = "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version", aliases = {"cp"})
public void compat(
@Param(description = "The pack folder name to report on, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_compat", defaultValue = "*")
String pack
) {
VolmitSender s = sender();
if (wantsAllPacks(pack)) {
Map<String, PackValidationResult> snapshot = PackValidationRegistry.snapshot();
if (snapshot.isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
return;
}
for (Map.Entry<String, PackValidationResult> entry : snapshot.entrySet()) {
for (String line : compatOutput(entry.getKey(), entry.getValue())) {
s.sendMessage(line);
}
}
return;
}
for (String line : compatOutput(pack, PackValidationRegistry.get(pack))) {
s.sendMessage(line);
}
}
/**
* The compat listing for one pack, straight from the persisted validation so no pack is reloaded. Every key is
* printed with every subject; the boot listing is the capped form of the same data.
*/
static List<String> compatOutput(String pack, PackValidationResult result) {
List<String> lines = new ArrayList<>();
if (result == null) {
lines.add(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE,
MessageArgument.untrusted("pack", String.valueOf(pack)),
MessageArgument.untrusted("pack2", String.valueOf(pack))));
return lines;
}
String version = compatVersion(result);
if (result.getCompatFindings().isEmpty()) {
lines.add(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_NONE,
MessageArgument.untrusted("pack", pack),
MessageArgument.untrusted("version", version)));
return lines;
}
lines.add(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_HEADER,
MessageArgument.untrusted("pack", pack),
MessageArgument.untrusted("version", version)));
for (String line : PackCompatReport.of(result.getCompatFindings()).keyLines(0)) {
lines.add(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2,
MessageArgument.untrusted("value", line)));
}
lines.add(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_COMPAT_REMEDY));
return lines;
}
/** The live Minecraft version, falling back to the one the validation ran against. */
static String compatVersion(PackValidationResult result) {
if (IrisPlatforms.isBound()) {
String live = IrisPlatforms.get().minecraftVersion();
if (live != null && !live.isBlank()) {
return live;
}
}
String persisted = result.getMinecraftVersion();
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
@@ -16,25 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.localization.IrisMessages;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.world.pregen.PregenTask;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.World;
import org.bukkit.util.Vector;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
public class CommandPregen implements DirectorExecutor {
@@ -62,7 +61,7 @@ public class CommandPregen implements DirectorExecutor {
if (PregeneratorJob.getInstance() != null) {
// Reject like the modded adapter does; never silently kill a running job.
sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
sender().sendMessage(IrisLanguage.text(IrisMessages.PREGEN_ALREADY_RUNNING));
return;
}
@@ -16,19 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorSystem;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.cache.AtomicCache;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.director.DirectorEngineOptions;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
@@ -55,18 +51,23 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.IrisMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
private static final String ROOT_COMMAND = "iris";
private static final String ROOT_PERMISSION = "iris.all";
private final ThreadLocal<Deque<LanguageAudience.Scope>> languageScopes = ThreadLocal.withInitial(ArrayDeque::new);
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient ThreadLocal<CommandSender> dispatchSenders = new ThreadLocal<>();
@@ -102,6 +103,12 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private DirectorContextRegistry buildDirectorContexts() {
DirectorContextRegistry contexts = new DirectorContextRegistry();
contexts.register(CommandSender.class, (invocation, map) -> {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
return sender.sender();
}
return null;
});
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
registerContextHandler(contexts, entry.getKey(), entry.getValue());
@@ -139,6 +146,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
@Override
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
languageScopes.get().push(LanguageAudience.open(sender.sender() instanceof Player player ? player.getUniqueId() : null));
DirectorContext.touch(new VolmitSender(sender.sender()));
}
}
@@ -146,6 +154,13 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
@Override
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
DirectorContext.remove();
Deque<LanguageAudience.Scope> scopes = languageScopes.get();
if (!scopes.isEmpty()) {
scopes.pop().close();
}
if (scopes.isEmpty()) {
languageScopes.remove();
}
}
@Nullable
@@ -169,7 +184,12 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
public void executeRoot(CommandSender sender, String label, String[] args) {
if (!sender.hasPermission(ROOT_PERMISSION)) {
if (args.length > 0 && args[0].equalsIgnoreCase("language")) {
Iris.instance.selectLanguage(sender, Arrays.copyOfRange(args, 1, args.length));
return;
}
if (!(args.length > 1 && args[0].equalsIgnoreCase("debug") && args[1].equalsIgnoreCase("dump"))
&& !sender.hasPermission(ROOT_PERMISSION)) {
ComponentMessenger.sendSection(sender, IrisLanguage.text(
IrisMessages.COMMAND_PERMISSION_DENIED,
MessageArgument.trusted("permission", ROOT_PERMISSION)
@@ -181,7 +201,9 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
List<String> suggestions = runDirectorTab(sender, alias, args);
List<String> suggestions = args.length > 0 && args[0].equalsIgnoreCase("language")
? Iris.instance.completeLanguage(sender, Arrays.copyOfRange(args, 1, args.length))
: runDirectorTab(sender, alias, args);
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
}
@@ -199,7 +221,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private void registerPaperCommand() {
try {
Class<?> registrarType = Class.forName(
"art.arcane.iris.core.commands.PaperCommandRegistrar",
"art.arcane.iris.command.PaperCommandRegistrar",
true,
getClass().getClassLoader()
);
@@ -235,35 +257,39 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
DirectorMiniMenu.deliver(
sender,
request.get(),
DirectorMiniMenu.Theme.irisGreen(),
IrisLanguage.directorResolver()
);
return true;
DirectorMiniMenu.deliver(
sender,
request.get(),
DirectorMiniMenu.Theme.irisGreen(),
IrisLanguage.directorResolver()
);
return true;
}
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender);
try {
DirectorContext.touch(new VolmitSender(sender));
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
} finally {
DirectorContext.remove();
dispatchSenders.remove();
}
}
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
DirectorContext.touch(new VolmitSender(sender));
try {
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
@@ -16,40 +16,39 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureCaptureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.BulkStructureImporter;
import art.arcane.iris.structure.StructureCaptureImporter;
import art.arcane.iris.structure.StructureImporter;
import art.arcane.iris.structure.StructureIndexService;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.PlacedStructurePiece;
import art.arcane.iris.structure.placement.StructureAssembler;
import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.structure.graph.StructureAssemblyResult;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.world.task.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
@@ -67,10 +66,10 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)", descriptionKey = "iris.director.commandstructure.director.iris_structure_tools_index_import_info")
public class CommandStructure implements DirectorExecutor {
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", descriptionKey = "iris.director.commandstructure.director.regenerate_structure_index_json_listing_all_vanilla_datapack_iris_structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
@@ -16,63 +16,63 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.ImageMapStudioGUI;
import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.FeatureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisNoiseGenerator;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.studio.view.GuiHost;
import art.arcane.iris.studio.view.ImageMapStudioGUI;
import art.arcane.iris.studio.view.NoiseExplorerGUI;
import art.arcane.iris.studio.view.VisionGUI;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.workspace.IrisProject;
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.iris.platform.bukkit.BoardSVC;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.structure.BulkStructureImporter;
import art.arcane.iris.structure.FeatureImporter;
import art.arcane.iris.structure.StructureImporter;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.loot.InventorySlotType;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisInterpolator;
import art.arcane.iris.world.loot.IrisLootTable;
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.command.handlers.DimensionHandler;
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
@@ -101,10 +101,10 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
public class CommandStudio implements DirectorExecutor {
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
@@ -275,10 +275,6 @@ public class CommandStudio implements DirectorExecutor {
if (noGUI()) {
return;
}
if (!ImageMapStudioGUI.isAvailable()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return;
}
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
return;
@@ -420,18 +416,6 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
}
@Director(description = "Package a dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
public void pkg(
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
boolean obfuscate,
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
boolean minify
) {
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
}
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
public void profile(
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
@@ -874,7 +858,7 @@ public class CommandStudio implements DirectorExecutor {
}
File ff = g.getData().getObjectLoader().findFile(i);
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
art.arcane.iris.generation.geometry.IrisBlockVector sz = IrisObject.sampleSize(ff);
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
stop.add(i);
} catch (Throwable e) {
@@ -896,6 +880,10 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return true;
}
if (!GuiHost.isAvailable()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DESKTOP_UNAVAILABLE));
return true;
}
return false;
}
@@ -16,29 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.studio.edit.BlockSignal;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.localization.C;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Chunk;
@@ -53,9 +53,9 @@ import org.bukkit.entity.Player;
import java.lang.reflect.Method;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?", descriptionKey = "iris.director.commandwhat.director.iris_what")
public class CommandWhat implements DirectorExecutor {
@@ -82,21 +82,36 @@ public class CommandWhat implements DirectorExecutor {
@Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER)
public void biome() {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> describeBiome(commandSender, player));
}
private void describeBiome(VolmitSender commandSender, Player player) {
Location location = player.getLocation();
try {
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
Engine engine = IrisToolbelt.access(location.getWorld()).getEngine();
IrisBiome b = engine.getBiome(location.getBlockX(), location.getBlockY() - location.getWorld().getMinHeight(), location.getBlockZ());
Biome derivative = b.getDerivative();
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
} catch (SavedBiomeUnavailableException e) {
if (e.getCause() != null) {
Iris.reportError(e);
}
commandSender.sendMessage(e.isLoading()
? "Saved biome data is loading. Run the command again shortly."
: e.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
Biome biome = player().getLocation().getBlock().getBiome();
Biome biome = location.getBlock().getBiome();
NamespacedKey key = resolveBiomeKey(biome);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
if (key == null || key.getKey().equals("custom")) {
try {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(player().getLocation())), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())))));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(location)), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(location)))));
} catch (Throwable ee) {
Iris.reportError(ee);
}
@@ -118,6 +133,13 @@ public class CommandWhat implements DirectorExecutor {
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
} catch (SavedBiomeUnavailableException e) {
if (e.getCause() != null) {
Iris.reportError(e);
}
commandSender.sendMessage(e.isLoading()
? "Saved biome data is loading. Run the command again shortly."
: e.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
@@ -1,8 +1,8 @@
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import java.util.Objects;
@@ -1,7 +1,6 @@
package art.arcane.iris.core.commands;
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.CommandSVC;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
@@ -11,7 +10,7 @@ import java.util.Collection;
import java.util.List;
/**
* Paper-only command registration. Lives outside {@code art.arcane.iris.core.service} on purpose:
* Paper-only command registration. Lives outside {@code art.arcane.iris.generation.runtime} on purpose:
* that package is eagerly Class.forName-scanned by JarScanner during enable, and resolving this
* class on plain Spigot throws NoClassDefFoundError for the Paper lifecycle types. CommandSVC
* loads it reflectively only when the Bukkit plugin.yml command is unavailable (Paper path).
@@ -1,50 +0,0 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import java.util.Optional;
public record IrisPapiWorldView(
IrisPapiPosition position,
long builtAtMs,
String available,
String biome,
String biomeKey,
String region,
String regionKey,
String dimension) {
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.FALSE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE);
}
public static IrisPapiWorldView present(
IrisPapiPosition position,
long builtAtMs,
Optional<String> biome,
Optional<String> biomeKey,
Optional<String> region,
Optional<String> regionKey,
Optional<String> dimension) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.TRUE,
text(biome),
text(biomeKey),
text(region),
text(regionKey),
text(dimension));
}
static String text(Optional<String> value) {
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
}
}
@@ -1,25 +0,0 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
public final class IrisSurfaceClassifier {
private IrisSurfaceClassifier() {
}
public static boolean requiresSurfaceBiome(int engineSurfaceHeight, int engineFluidHeight) {
return engineSurfaceHeight > 0 && engineSurfaceHeight > engineFluidHeight;
}
public static IrisSurfaceKind classify(int engineSurfaceHeight, int engineFluidHeight, InferredType inferredType) {
if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID;
}
if (engineSurfaceHeight <= engineFluidHeight) {
return IrisSurfaceKind.OCEAN;
}
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
}
}
@@ -1,24 +1,21 @@
package art.arcane.iris.core.service;
package art.arcane.iris.generation.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.lifecycle.WorldUnloadBoundaryRegistry;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.world.lifecycle.WorldUnloadBoundaryRegistry;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.world.pregen.MantleHeapPressure;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.CachedStream3D;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
@@ -153,7 +150,7 @@ public final class IrisEngineSVC implements IrisService {
metrics.get()));
}
IrisTelemetrySnapshot telemetrySnapshot() {
public IrisTelemetrySnapshot telemetrySnapshot() {
return telemetry.get();
}
@@ -1,19 +1,18 @@
package art.arcane.iris.core.service;
package art.arcane.iris.generation.runtime;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.CachedStream3D;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
final class IrisEngineStatus {
private IrisEngineStatus() {
@@ -1,10 +1,9 @@
package art.arcane.iris.core.service;
package art.arcane.iris.generation.runtime;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import java.util.List;
record IrisTelemetrySnapshot(
public record IrisTelemetrySnapshot(
long sampledAtMs,
List<EngineTelemetrySnapshot> worlds,
EngineTelemetrySnapshot.Aggregate aggregate,
@@ -17,7 +16,7 @@ record IrisTelemetrySnapshot(
CacheSnapshot caches,
PregenSnapshot pregenerator
) {
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
public static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
0L,
List.of(),
EngineTelemetrySnapshot.aggregate(List.of()),
@@ -31,7 +30,7 @@ record IrisTelemetrySnapshot(
PregenSnapshot.INACTIVE
);
IrisTelemetrySnapshot {
public IrisTelemetrySnapshot {
worlds = worlds == null ? List.of() : List.copyOf(worlds);
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
@@ -48,14 +47,14 @@ record IrisTelemetrySnapshot(
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
}
record CacheSnapshot(
public record CacheSnapshot(
CacheBucket total,
CacheBucket resource,
CacheBucket stream2d,
CacheBucket stream3d,
CacheBucket other
) {
static final CacheSnapshot EMPTY = new CacheSnapshot(
public static final CacheSnapshot EMPTY = new CacheSnapshot(
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
@@ -63,7 +62,7 @@ record IrisTelemetrySnapshot(
CacheBucket.EMPTY
);
CacheSnapshot {
public CacheSnapshot {
total = total == null ? CacheBucket.EMPTY : total;
resource = resource == null ? CacheBucket.EMPTY : resource;
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
@@ -72,16 +71,16 @@ record IrisTelemetrySnapshot(
}
}
record CacheBucket(int count, long entries, long capacity) {
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
public record CacheBucket(int count, long entries, long capacity) {
public static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
CacheBucket {
public CacheBucket {
count = Math.max(0, count);
entries = Math.max(0L, entries);
capacity = Math.max(0L, capacity);
}
double usage() {
public double usage() {
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
}
@@ -93,7 +92,7 @@ record IrisTelemetrySnapshot(
}
}
record PregenSnapshot(
public record PregenSnapshot(
boolean active,
String worldIdentity,
String worldName,
@@ -107,7 +106,7 @@ record IrisTelemetrySnapshot(
long elapsedMs,
long failed
) {
static final PregenSnapshot INACTIVE = new PregenSnapshot(
public static final PregenSnapshot INACTIVE = new PregenSnapshot(
false,
"",
"",
@@ -122,7 +121,7 @@ record IrisTelemetrySnapshot(
0L
);
PregenSnapshot {
public PregenSnapshot {
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
worldName = worldName == null ? "" : worldName.trim();
progressPercent = Double.isFinite(progressPercent)
@@ -1,4 +1,6 @@
package art.arcane.iris.core.service;
package art.arcane.iris.generation.runtime;
import art.arcane.iris.platform.bukkit.api.IrisApiEventSVC;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
@@ -1,9 +1,12 @@
package art.arcane.iris.core.service;
package art.arcane.iris.integration;
import art.arcane.iris.generation.runtime.IrisEngineSVC;
import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.link;
package art.arcane.iris.integration;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry;
import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion;
@@ -42,6 +42,19 @@ public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
.key("world.available", state::worldAvailable)
.key("world.biome", state::biome)
.key("world.biome-key", state::biomeKey)
.key("world.biome-custom", state::biomeCustom)
.key("world.biome-custom-id", state::biomeCustomId)
.key("world.biome-custom-ids", state::biomeCustomIds)
.key("world.biome-custom-key", state::biomeCustomKey)
.key("world.biome-custom-keys", state::biomeCustomKeys)
.key("world.biome-custom-count", state::biomeCustomCount)
.key("world.biome-derivative", state::biomeDerivative)
.key("world.biome-vanilla-derivative", state::biomeVanillaDerivative)
.key("world.biome-type", state::biomeType)
.key("world.min-height", state::minHeight)
.key("world.max-height", state::maxHeight)
.key("world.height", state::height)
.key("world.fluid-height", state::fluidHeight)
.key("world.region", state::region)
.key("world.region-key", state::regionKey)
.key("world.dimension", state::dimension)

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